As we'll be using the browser as our demo environment, let's see a couple of functions to interact with the user:
alert,
prompt and
confirm.
This one we've seen already. It shows a message and waits for the user to press "OK".
For example:
js run alert("Hello");
The mini-window with the message is called a modal window. The word "modal" means that the visitor can't interact with the rest of the page, press other buttons, etc, until they have dealt with the window. In this case - until they press "OK".
The function
prompt accepts two arguments:
js no-beautify result = prompt(title, [default]);
It shows a modal window with a text message, an input field for the visitor, and the buttons OK/Cancel.
title
default
smart header="The square brackets in syntax `[...]`" The square brackets around `default` in the syntax above denote that the parameter is optional, not required.
The visitor can type something in the prompt input field and press OK. Then we get that text in the
result. Or they can cancel the input by pressing Cancel or hitting the
key:Esc key, then we get
null as the
result.
The call to
prompt returns the text from the input field or
null if the input was canceled.
For instance:
run let age = prompt(‘How old are you?''', 100);
alert(
You are ${age} years old!); // You are 100 years old!
warn header="In IE: always supply adefault
" The second parameter is optional, but if we don't supply it, Internet Explorer will insert the text"undefined"`
into the prompt.
Run this code in Internet Explorer to see:
js run let test = prompt("Test");
So, for prompts to look good in IE, we recommend always providing the second argument:
js run let test = prompt("Test", ''); // <-- for IE
The syntax:
The function
confirm shows a modal window with a
question and two buttons: OK and Cancel.
The result is
true if OK is pressed and
false otherwise.
For example:
run let isBoss = confirm("Are you the boss?");
alert( isBoss ); // true if OK is pressed
We covered 3 browser-specific functions to interact with visitors:
alert
prompt
key:Esc is clicked,
null.
confirm
true for OK and
false for Cancel/
key:Esc.
All these methods are modal: they pause script execution and don't allow the visitor to interact with the rest of the page until the window has been dismissed.
There are two limitations shared by all the methods above:
That is the price for simplicity. There are other ways to show nicer windows and richer interaction with the visitor, but if "bells and whistles" do not matter much, these methods work just fine.
Before we get into JavaScript's ways of dealing with styles and classes - here's an important rule. Hopefully it's obvious enough, but we still have to mention it.
There are generally two ways to style an element:
<div class="...">
style:
<div style="...">.
JavaScript can modify both classes and
style properties.
We should always prefer CSS classes to
style. The latter should only be used if classes "can't handle it".
For example,
style is acceptable if we calculate coordinates of an element dynamically and want to set them from
JavaScript, like this:
let top
=
/* complex calculations */
;
let left
=
/* complex calculations */
;
elem.
style.
left
= left
;
// e.g '123px', calculated at run-time
elem.
style.
top
= top
;
// e.g '456px'
For other cases, like making the text red, adding a background icon - describe that in CSS and then add the class (JavaScript can do that). That's more flexible and easier to support.
Changing a class is one of the most often used actions in scripts.
In the ancient time, there was a limitation in JavaScript: a reserved word like
"class" could not be an object property. That limitation does not exist now, but at that time it was
impossible to have a
"class" property, like
elem.class.
So for classes the similar-looking property
"className" was introduced: the
elem.className corresponds to the
"class" attribute.
For instance:
html run <body class="main page"> <script> alert(document.body.className); // main page </script> </body>
If we assign something to
elem.className, it replaces the whole string of classes. Sometimes that's what we need, but often we
want to add/remove a single class.
There's another property for that:
elem.classList.
The
elem.classList is a special object with methods to
add/remove/toggle a single class.
For instance:
run
So we can operate both on the full class string using
className or on individual classes using
classList. What we choose depends on our needs.
Methods of
classList:
elem.classList.add/remove("class") - adds/removes the class.
elem.classList.toggle("class") - adds the class if it doesn't exist, otherwise removes it.
elem.classList.contains("class") - checks for the given class, returns
true/false.
Besides,
classList is iterable, so we can list all classes with
for..of, like this:
html run <body class="main page"> <script> for (let name of document.body.classList) { alert(name); // main, and then page } </script> </body>
The property
elem.style is an object that corresponds to what's written in the
"style" attribute. Setting
elem.style.width="100px" works the same as if we had in the attribute
style a string
width:100px.
For multi-word property the camelCase is used:
js no-beautify background-color => elem.style.backgroundColor z-index => elem.style.zIndex border-left-width => elem.style.borderLeftWidth
For instance:
js run document.body.style.backgroundColor = prompt('background color?', 'green');
smart header="Prefixed properties" Browser-prefixed properties like-moz-border-radius
,-webkit-border-radius` also follow the same rule: a dash means upper case.
For instance:
Sometimes we want to assign a style property, and later remove it.
For instance, to hide an element, we can set
elem.style.display = "none".
Then later we may want to remove the
style.display as if it were not set. Instead of
delete elem.style.display we should assign an empty string to it:
elem.style.display = "".
So, modifying a style is easy. But how to read it?
For instance, we want to know the size, margins, the color of an element. How to do it?
The
style property operates only on the value of the
"style" attribute, without any CSS cascade.
So we can't read anything that comes from CSS classes using
elem.style.
For instance, here
style doesn't see the margin:
…But what if we need, say, to increase the margin by
20px? We would want the current value of it.
There's another method for that:
getComputedStyle.
The syntax is:
::before. An empty string or no argument means the element itself.
The result is an object with styles, like
elem.style, but now with respect to all CSS classes.
For instance:
run height=100smart header="Computed and resolved values" There are two concepts in CSS:
height:1em or
font-size:125%.
1em or
125% are relative. The browser takes the computed value and makes all units fixed and absolute, for
instance:
height:20px or
font-size:16px. For geometry properties resolved values may have a floating point, like
width:50.5px.
A long time ago
getComputedStyle was created to get computed values, but it turned out that resolved values are much
more convenient, and the standard changed.
So nowadays
getComputedStyle actually returns the resolved value of the property, usually in
warn header="getComputedStyle
requires the full property name" We should always ask for the exact property that we want, likepaddingLeft
ormarginTop
orborderTopWidth`. Otherwise the correct result is not guaranteed.
For instance, if there are properties
paddingLeft/paddingTop, then what should we get for
getComputedStyle(elem).padding? Nothing, or maybe a "generated" value from known paddings? There's no
standard rule here.
There are other inconsistencies. As an example, some browsers (Chrome) show
10px in the document below, and some of them (Firefox) - do not:
html run <style> body { margin: 10px; } </style> <script> let style = getComputedStyle(document.body); alert(style.margin); // empty string in Firefox </script>
``
smart header="Styles applied to:visited
links are hidden!" Visited links may be colored using:visited` CSS pseudoclass.
But
getComputedStyle does not give access to that color, because otherwise an arbitrary page could find
out whether the user visited a link by creating it on the page and checking the styles.
JavaScript may not see the styles applied by
:visited. And also, there's a limitation in CSS that forbids applying geometry-changing styles in
:visited. That's to guarantee that there's no side way for an evil page to test if a link was visited
and hence to break the privacy.
To manage classes, there are two DOM properties:
className - the string value, good to manage the whole set of classes.
classList - the object with methods
add/remove/toggle/contains, good for individual classes.
To change the styles:
The
style property is an object with camelCased styles. Reading and writing to it has the same
meaning as modifying individual properties in the
"style" attribute. To see how to apply
important and other rare stuff - there's a list of methods at
MDN.
The
style.cssText property corresponds to the whole
"style" attribute, the full string of styles.
To read the resolved styles (with respect to all classes, after all CSS is applied and final values are calculated):
getComputedStyle(elem, [pseudo]) returns the style-like object with them. Read-only.
There are many JavaScript properties that allow us to read information about element width, height and other geometry features.
We often need them when moving or positioning elements in JavaScript.
As a sample element to demonstrate properties we'll use the one given below:
html no-beautify <div id="example"> ...Text... </div> <style> #example { width: 300px; height: 200px; border: 25px solid #E8C48F; padding: 20px; overflow: auto; } </style>
It has the border, padding and scrolling. The full set of features. There are no margins, as they are not the part of the element itself, and there are no special properties for them.
The element looks like this:
You can open the document in the sandbox.
smart header="Mind the scrollbar" The picture above demonstrates the most complex case when the element has a scrollbar. Some browsers (not all) reserve the space for it by taking it from the content (labeled as "content width" above).
So, without scrollbar the content width would be
300px, but if the scrollbar is
16px wide (the width may vary between devices and browsers) then only
300 - 16 = 284px remains, and we should take it into account. That's why examples from this chapter
assume that there's a scrollbar. Without it, some calculations are simpler.
smart header="The `padding-bottom` area may be filled with text" Usually paddings are shown empty on our illustrations, but if there's a lot of text in the element and it overflows, then browsers show the "overflowing" text at `padding-bottom`, that's normal.
Here's the overall picture with geometry properties:
Values of these properties are technically numbers, but these numbers are "of pixels", so these are pixel measurements.
Let's start exploring the properties starting from the outside of the element.
These properties are rarely needed, but still they are the "most outer" geometry properties, so we'll start with them.
The
offsetParent is the nearest ancestor that the browser uses for calculating coordinates during
rendering.
That's the nearest ancestor that is one of the following:
position is
absolute,
relative,
fixed or
sticky), or
<td>,
<th>, or
<table>, or
<body>.
Properties
offsetLeft/offsetTop provide x/y coordinates relative to
offsetParent upper-left corner.
In the example below the inner
<div> has
<main> as
offsetParent and
offsetLeft/offsetTop shifts from its upper-left corner (
180):
html run height=10 <main style="position: relative" id="main"> <article> <div id="example" style="position: absolute; left: 180px; top: 180px">...</div> </article> </main> <script> alert(example.offsetParent.id); // main alert(example.offsetLeft); // 180 (note: a number, not a string "180px") alert(example.offsetTop); // 180 </script>
There are several occasions when
offsetParent is
null:
display:none or not in the document).
<body> and
<html>.
position:fixed.
Now let's move on to the element itself.
These two properties are the simplest ones. They provide the outer" width/height of the element. Or, in other words, its full size including borders.
For our sample element:
offsetWidth = 390 - the outer width, can be calculated as inner CSS-width (
300px) plus paddings (
2 * 20px) and borders (
2 * 25px).
offsetHeight = 290 - the outer height.
smart header="Geometry properties are zero/null for elements that are not displayed" Geometry properties are calculated only for displayed elements.
If an element (or any of its ancestors) has
display:none or is not in the document, then all geometry properties are zero (or
null for
offsetParent).
For example,
offsetParent is
null, and
offsetWidth,
offsetHeight are
0 when we created an element, but haven't inserted it into the document yet, or it (or it's ancestor)
has
display:none.
We can use this to check if an element is hidden, like this:
Please note that such
isHidden returns
true for elements that are on-screen, but have zero sizes (like an empty
<div>).
Inside the element we have the borders.
To measure them, there are properties
clientTop and
clientLeft.
In our example:
clientLeft = 25 - left border width
clientTop = 25 - top border width
…But to be precise - these properties are not border width/height, but rather relative coordinates of the inner side from the outer side.
What's the difference?
It becomes visible when the document is right-to-left (the operating system is in Arabic or Hebrew languages).
The scrollbar is then not on the right, but on the left, and then
clientLeft also includes the scrollbar width.
In that case,
clientLeft would be not
25, but with the scrollbar width
25 + 16 = 41.
Here's the example in hebrew:
These properties provide the size of the area inside the element borders.
They include the content width together with paddings, but without the scrollbar:
On the picture above let's first consider
clientHeight.
There's no horizontal scrollbar, so it's exactly the sum of what's inside the borders: CSS-height
200px plus top and bottom paddings (
2 * 20px) total
240px.
Now
clientWidth - here the content width is not
300px, but
284px, because
16px are occupied by the scrollbar. So the sum is
284px plus left and right paddings, total
324px.
If there are no paddings, then
clientWidth/Height is exactly the content area, inside the borders and the scrollbar (if any).
So when there's no padding we can use
clientWidth/clientHeight to get the content area size.
These properties are like
clientWidth/clientHeight, but they also include the scrolled out (hidden) parts:
On the picture above:
scrollHeight = 723 - is the full inner height of the content area including the scrolled out parts.
scrollWidth = 324 - is the full inner width, here we have no horizontal scroll, so it equals
clientWidth.
We can use these properties to expand the element wide to its full width/height.
Like this:
// expand the element to the full content height
element.
style.
height
=
`
${
element.
scrollHeight
}
px`
;
Click the button to expand the element:
<div id="element" style="width:300px;height:200px; padding: 0;overflow: auto; border:1px solid black;">text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text</div>
<button style="padding:0" onclick="element.style.height = `${element.scrollHeight}px`">element.style.height = `${element.scrollHeight}px`</button>
Properties
scrollLeft/scrollTop are the width/height of the hidden, scrolled out part of the element.
On the picture below we can see
scrollHeight and
scrollTop for a block with a vertical scroll.
In other words,
scrollTop is "how much is scrolled up".
smart header="scrollLeft/scrollTop
can be modified" Most of the geometry properties here are read-only, butscrollLeft/scrollTop` can be
changed, and the browser will scroll the element.
If you click the element below, the code `elem.scrollTop += 10` executes. That makes the element content scroll `10px` down.
<div onclick="this.scrollTop+=10" style="cursor:pointer;border:1px solid black;width:100px;height:80px;overflow:auto">Click<br>Me<br>1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>9</div>
Setting
scrollTop to
0 or a big value, such as
1e9 will make the element scroll to the very top/bottom respectively.
We've just covered geometry properties of DOM elements, that can be used to get widths, heights and calculate distances.
But as we know from the chapter
info:styles-and-classes, we can read CSS-height and width using
getComputedStyle.
So why not to read the width of an element with
getComputedStyle, like this?
run let elem = document.body;
alert( getComputedStyle(elem).width ); // show CSS width for elem
Why should we use geometry properties instead? There are two reasons:
width/height depend on another property:
box-sizing that defines "what is" CSS width and height. A change in
box-sizing for CSS purposes may break such JavaScript.
Second, CSS
width/height may be
auto, for instance for an inline element:
run Hello!
From the CSS standpoint,
width:auto is perfectly normal, but in JavaScript we need an exact size in
px that we can use in calculations. So here CSS width is useless.
And there's one more reason: a scrollbar. Sometimes the code that works fine without a scrollbar becomes buggy
with it, because a scrollbar takes the space from the content in some browsers. So the real width available for
the content is
less than CSS width. And
clientWidth/clientHeight take that into account.
…But with
getComputedStyle(elem).width the situation is different. Some browsers (e.g. Chrome) return the real
inner width, minus the scrollbar, and some of them (e.g. Firefox) - CSS width (ignore the scrollbar). Such
cross-browser differences is the reason not to use
getComputedStyle, but rather rely on geometry properties.
If your browser reserves the space for a scrollbar (most browsers for Windows do), then you can test it below.
[iframe src="cssWidthScroll" link border=1]
The element with text has CSS `width:300px`.
On a Desktop Windows OS, Firefox, Chrome, Edge all reserve the space for the scrollbar. But Firefox shows `300px`, while Chrome and Edge show less. That's because Firefox returns the CSS width and other browsers return the "real" width.
Please note that the described difference is only about reading
getComputedStyle(...).width from JavaScript, visually everything is correct.
Elements have the following geometry properties:
offsetParent - is the nearest positioned ancestor or
td,
th,
table,
body.
offsetLeft/offsetTop - coordinates relative to the upper-left edge of
offsetParent.
offsetWidth/offsetHeight - "outer" width/height of an element including borders.
clientLeft/clientTop - the distances from the upper-left outer corner to the upper-left inner
(content + padding) corner. For left-to-right OS they are always the widths of left/top borders. For
right-to-left OS the vertical scrollbar is on the left so
clientLeft includes its width too.
clientWidth/clientHeight - the width/height of the content including paddings, but without the
scrollbar.
scrollWidth/scrollHeight - the width/height of the content, just like
clientWidth/clientHeight, but also include scrolled-out, invisible part of the element.
scrollLeft/scrollTop - width/height of the scrolled out upper part of the element, starting from
its upper-left corner.
All properties are read-only except
scrollLeft/scrollTop that make the browser scroll the element if changed.
How do we find the width and height of the browser window? How do we get the full width and height of the document, including the scrolled out part? How do we scroll the page using JavaScript?
For this type of information, we can use the root document element
document.documentElement, that corresponds to the
<html> tag. But there are additional methods and peculiarities to consider.
To get window width and height, we can use the
clientWidth/clientHeight of
document.documentElement:
For instance, this button shows the height of your window:
<button onclick="alert(document.documentElement.clientHeight)">alert(document.documentElement.clientHeight)</button>
warn header="Notwindow.innerWidth/innerHeight
" Browsers also support properties likewindow.innerWidth/innerHeight`. They look like what we want,
so why not to use them instead?
If there exists a scrollbar, and it occupies some space,
clientWidth/clientHeight provide the width/height without it (subtract it). In other words, they
return the width/height of the visible part of the document, available for the content.
window.innerWidth/innerHeight includes the scrollbar.
If there's a scrollbar, and it occupies some space, then these two lines show different values:
js run alert( window.innerWidth ); // full window width alert( document.documentElement.clientWidth ); // window width minus the scrollbar
In most cases, we need the
available window width in order to draw or position something within scrollbars (if there are any), so we
should use
documentElement.clientHeight/clientWidth.
``
warn header="DOCTYPE
is important" Please note: top-level geometry properties may work a little bit differently when there's no<!DOCTYPE
HTML>` in HTML. Odd things are possible.
In modern HTML we should always write
DOCTYPE.
Theoretically, as the root document element is
document.documentElement, and it encloses all the content, we could measure the document's full size
as
document.documentElement.scrollWidth/scrollHeight.
But on that element, for the whole page, these properties do not work as intended. In Chrome/Safari/Opera, if
there's no scroll, then
documentElement.scrollHeight may be even less than
documentElement.clientHeight! Weird, right?
To reliably obtain the full document height, we should take the maximum of these properties:
run let scrollHeight = Math.max( document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight, document.body.clientHeight, document.documentElement.clientHeight );
alert(‘Full document height, with scrolled out part:''' + scrollHeight);
Why so? Better don't ask. These inconsistencies come from ancient times, not a "smart" logic.
DOM elements have their current scroll state in their
scrollLeft/scrollTop properties.
For document scroll,
document.documentElement.scrollLeft/scrollTop works in most browsers, except older WebKit-based ones,
like Safari (bug
5991), where we should use
document.body instead of
document.documentElement.
Luckily, we don't have to remember these peculiarities at all, because the scroll is available in the special
properties,
window.pageXOffset/pageYOffset:
js run alert('Current scroll from the top: ' + window.pageYOffset); alert('Current scroll from the left: ' + window.pageXOffset);
These properties are read-only.
smart header="Also available as `window` properties `scrollX` and `scrollY`" For historical reasons, both properties exist, but they are the same: - `window.pageXOffset` is an alias of `window.scrollX`. - `window.pageYOffset` is an alias of `window.scrollY`.
To scroll the page with JavaScript, its DOM must be fully built.
For instance, if we try to scroll the page with a script in `<head>`, it won't work.
Regular elements can be scrolled by changing
scrollTop/scrollLeft.
We can do the same for the page using
document.documentElement.scrollTop/scrollLeft (except Safari, where
document.body.scrollTop/Left should be used instead).
Alternatively, there's a simpler, universal solution: special methods window.scrollBy(x,y) and window.scrollTo(pageX,pageY).
The method
scrollBy(x,y) scrolls the page
relative to its current position. For instance,
scrollBy(0,10) scrolls the page
10px down.
The button below demonstrates this:
<button onclick="window.scrollBy(0,10)">window.scrollBy(0,10)</button>
The method
scrollTo(pageX,pageY) scrolls the page
to absolute coordinates, so that the top-left corner of the visible part has coordinates
(pageX, pageY) relative to the document's top-left corner. It's like setting
scrollLeft/scrollTop.
To scroll to the very beginning, we can use
scrollTo(0,0).
<button onclick="window.scrollTo(0,0)">window.scrollTo(0,0)</button>
These methods work for all browsers the same way.
For completeness, let's cover one more method: elem.scrollIntoView(top).
The call to
elem.scrollIntoView(top) scrolls the page to make
elem visible. It has one argument:
top=true (that's the default), then the page will be scrolled to make
elem appear on the top of the window. The upper edge of the element will be aligned with the window
top.
top=false, then the page scrolls to make
elem appear at the bottom. The bottom edge of the element will be aligned with the window bottom.
The button below scrolls the page to position itself at the window top:
<button onclick="this.scrollIntoView()">this.scrollIntoView()</button>
And this button scrolls the page to position itself at the bottom:
<button onclick="this.scrollIntoView(false)">this.scrollIntoView(false)</button>
Sometimes we need to make the document "unscrollable". For instance, when we need to cover the page with a large message requiring immediate attention, and we want the visitor to interact with that message, not with the document.
To make the document unscrollable, it's enough to set
document.body.style.overflow = "hidden". The page will "freeze" at its current scroll position.
Try it:
<button onclick="document.body.style.overflow = 'hidden'">document.body.style.overflow = 'hidden'</button>
<button onclick="document.body.style.overflow = ''">document.body.style.overflow = ''</button>
The first button freezes the scroll, while the second one releases it.
We can use the same technique to freeze the scroll for other elements, not just for
document.body.
The drawback of the method is that the scrollbar disappears. If it occupied some space, then that space is now free and the content "jumps" to fill it.
That looks a bit odd, but can be worked around if we compare
clientWidth before and after the freeze. If it increased (the scrollbar disappeared), then add
padding to
document.body in place of the scrollbar to keep the content width the same.
Geometry:
document.documentElement.clientWidth/clientHeight
Width/height of the whole document, with the scrolled out part:
Scrolling:
window.pageYOffset/pageXOffset.
Change the current scroll:
window.scrollTo(pageX,pageY) - absolute coordinates,
window.scrollBy(x,y) - scroll relative the current place,
elem.scrollIntoView(top) - scroll to make
elem visible (align with the top/bottom of the window).
To move elements around we should be familiar with coordinates.
Most JavaScript methods deal with one of two coordinate systems:
position:fixed, calculated from the window top/left edge.
clientX/clientY, the reasoning for such name will become clear later, when we study event
properties.
position:absolute in the document root, calculated from the document top/left edge.
pageX/pageY.
When the page is scrolled to the very beginning, so that the top/left corner of the window is exactly the document top/left corner, these coordinates equal each other. But after the document shifts, window-relative coordinates of elements change, as elements move across the window, while document-relative coordinates remain the same.
On this picture we take a point in the document and demonstrate its coordinates before the scroll (left) and after it (right):
When the document scrolled: -
pageY - document-relative coordinate stayed the same, it's counted from the document top (now
scrolled out). -
clientY - window-relative coordinate did change (the arrow became shorter), as the same point became
closer to window top.
The method
elem.getBoundingClientRect() returns window coordinates for a minimal rectangle that encloses
elem as an object of built-in
DOMRect class.
Main
DOMRect properties:
x/y - X/Y-coordinates of the rectangle origin relative to window,
width/height - width/height of the rectangle (can be negative).
Additionally, there are derived properties:
top/bottom - Y-coordinate for the top/bottom rectangle edge,
left/right - X-coordinate for the left/right rectangle edge.
For instance click this button to see its window coordinates:
<p><input id="brTest" type="button" value="Get coordinates using button.getBoundingClientRect() for this button" onclick='showRect(this)'/></p>
<script>
function showRect(elem) {
let r = elem.getBoundingClientRect();
alert(`x:${r.x}
y:${r.y}
width:${r.width}
height:${r.height}
top:${r.top}
bottom:${r.bottom}
left:${r.left}
right:${r.right}
`);
}
</script>
If you scroll the page and repeat, you'll notice that as window-relative button position changes, its window coordinates (`y/top/bottom` if you scroll vertically) change as well.
Here's the picture of
elem.getBoundingClientRect() output:
As you can see,
x/y and
width/height fully describe the rectangle. Derived properties can be easily calculated from them:
left = x
top = y
right = x + width
bottom = y + height
Please note:
10.5. That's normal, internally browser uses fractions in calculations. We don't have to round them
when setting to
style.left/top.
elem is now above the window, then
elem.getBoundingClientRect().top is negative.
``
smart header="Why derived properties are needed? Why doestop/left
exist if there'sx/y
?" Mathematically, a rectangle is uniquely defined with its starting point(x,y)
and the direction vector(width,height)`. So the additional derived properties are for convenience.
Technically it's possible for
width/height to be negative, that allows for "directed" rectangle, e.g. to represent mouse selection
with properly marked start and end.
Negative
width/height values mean that the rectangle starts at its bottom-right corner and then "grows"
left-upwards.
Here's a rectangle with negative
width and
height (e.g.
width=-200,
height=-100):
As you can see,
left/top do not equal
x/y in such case.
In practice though,
elem.getBoundingClientRect() always returns positive width/height, here we mention negative
width/height only for you to understand why these seemingly duplicate properties are not actually
duplicates.
``
warn header="Internet Explorer: no support forx/y
" Internet Explorer doesn't supportx/y` properties for historical reasons.
So we can either make a polyfill (add getters in
DomRect.prototype) or just use
top/left, as they are always the same as
x/y for positive
width/height, in particular in the result of
elem.getBoundingClientRect().
``
warn header="Coordinates right/bottom are different from CSS position properties" There are obvious similarities between window-relative coordinates and CSSposition:fixed`.
But in CSS positioning,
right property means the distance from the right edge, and
bottom property means the distance from the bottom edge.
If we just look at the picture above, we can see that in JavaScript it is not so. All window coordinates are counted from the top-left corner, including these ones.
The call to
document.elementFromPoint(x, y) returns the most nested element at window coordinates
(x, y).
The syntax is:
For instance, the code below highlights and outputs the tag of the element that is now in the middle of the window:
run let centerX = document.documentElement.clientWidth / 2; let centerY = document.documentElement.clientHeight / 2;
let elem = document.elementFromPoint(centerX, centerY);
elem.style.background = "red"; alert(elem.tagName);
As it uses window coordinates, the element may be different depending on the current scroll position.
warn header="For out-of-window coordinates theelementFromPoint
returnsnull
" The methoddocument.elementFromPoint(x,y)
only works if(x,y)` are inside the visible area.
If any of the coordinates is negative or exceeds the window width/height, then it returns
null.
Here's a typical error that may occur if we don't check for it:
let elem
=
document.
elementFromPoint(x
, y)
;
// if the coordinates happen to be out of the window, then elem = null
*!*
elem.
style.
background
=
''
;
// Error!
*
/!
*
Most of time we need coordinates in order to position something.
To show something near an element, we can use
getBoundingClientRect to get its coordinates, and then CSS
position together with
left/top (or
right/bottom).
For instance, the function
createMessageUnder(elem, html) below shows the message under
elem:
let elem
=
document.
getElementById(
"coords-show-mark")
;
function
createMessageUnder(elem
, html)
{
// create message element
let message
=
document.
createElement(
'div')
;
// better to use a css class for the style here
message.
style.
cssText
=
"position:fixed; color: red"
;
*!*
// assign coordinates, don't forget "px"!
let coords
=
elem.
getBoundingClientRect()
;
message.
style.
left
=
coords.
left
+
"px"
;
message.
style.
top
=
coords.
bottom
+
"px"
;
*
/!
*
message.innerHTML = html;
return message;
}
// Usage:
// add it for 5 seconds in the document
let message = createMessageUnder
(
elem, 'Hello, world!'
)
;
document.body.append
(
message
)
;
setTimeout
(()
=> message.remove
()
, 5000
)
;
Click the button to run it:
<button id="coords-show-mark">Button with id="coords-show-mark", the message will appear under it</button>
The code can be modified to show the message at the left, right, below, apply CSS animations to "fade it in" and so on. That's easy, as we have all the coordinates and sizes of the element.
But note the important detail: when the page is scrolled, the message flows away from the button.
The reason is obvious: the message element relies on
position:fixed, so it remains at the same place of the window while the page scrolls away.
To change that, we need to use document-based coordinates and
position:absolute.
Document-relative coordinates start from the upper-left corner of the document, not the window.
In CSS, window coordinates correspond to
position:fixed, while document coordinates are similar to
position:absolute on top.
We can use
position:absolute and
top/left to put something at a certain place of the document, so that it remains there during a page
scroll. But we need the right coordinates first.
There's no standard method to get the document coordinates of an element. But it's easy to write it.
The two coordinate systems are connected by the formula: -
pageY =
clientY + height of the scrolled-out vertical part of the document. -
pageX =
clientX + width of the scrolled-out horizontal part of the document.
The function
getCoords(elem) will take window coordinates from
elem.getBoundingClientRect() and add the current scroll to them:
// get document coordinates of the element
function
getCoords(elem)
{
let box
=
elem.
getBoundingClientRect()
;
return
{
top
:
box.
top
+
window.
pageYOffset
,
right
:
box.
right
+
window.
pageXOffset
,
bottom
:
box.
bottom
+
window.
pageYOffset
,
left
:
box.
left
+
window.
pageXOffset
};
}
If in the example above we used it with
position:absolute, then the message would stay near the element on scroll.
The modified
createMessageUnder function:
function
createMessageUnder(elem
, html)
{
let message
=
document.
createElement(
'div')
;
message.
style.
cssText
=
"*!*position:absolute*/!*; color: red"
;
let coords
=
*!*
getCoords(elem)
;*
/!
*
message.style.left = coords.left
+
"px";
message.style.top = coords.bottom
+
"px";
message.innerHTML = html;
return message;
}
Any point on the page has coordinates:
elem.getBoundingClientRect().
elem.getBoundingClientRect() plus the current page scroll.
Window coordinates are great to use with
position:fixed, and document coordinates do well with
position:absolute.
Both coordinate systems have their pros and cons; there are times we need one or the other one, just like CSS
position
absolute and
fixed.
An event is a signal that something has happened. All DOM nodes generate such signals (but events are not limited to DOM).
Here's a list of the most useful DOM events, just to take a look at:
Mouse events: -
click - when the mouse clicks on an element (touchscreen devices generate it on a tap). -
contextmenu - when the mouse right-clicks on an element. -
mouseover /
mouseout - when the mouse cursor comes over / leaves an element. -
mousedown /
mouseup - when the mouse button is pressed / released over an element. -
mousemove - when the mouse is moved.
Keyboard events: -
keydown and
keyup - when a keyboard key is pressed and released.
Form element events: -
submit - when the visitor submits a
<form>. -
focus - when the visitor focuses on an element, e.g. on an
<input>.
Document events: -
DOMContentLoaded - when the HTML is loaded and processed, DOM is fully built.
CSS events: -
transitionend - when a CSS-animation finishes.
There are many other events. We'll get into more details of particular events in next chapters.
To react on events we can assign a handler - a function that runs in case of an event.
Handlers are a way to run JavaScript code in case of user actions.
There are several ways to assign a handler. Let's see them, starting from the simplest one.
A handler can be set in HTML with an attribute named
on<event>.
For instance, to assign a
click handler for an
input, we can use
onclick, like here:
html run <input value="Click me" *!*onclick="alert('Click!')"*/!* type="button">
On mouse click, the code inside
onclick runs.
Please note that inside
onclick we use single quotes, because the attribute itself is in double quotes. If we forget that the
code is inside the attribute and use double quotes inside, like this:
onclick="alert("Click!")", then it won't work right.
An HTML-attribute is not a convenient place to write a lot of code, so we'd better create a JavaScript function and call it there.
Here a click runs the function
countRabbits():
<input type="button" !onclick="countRabbits()" /! value="Count rabbits!">
As we know, HTML attribute names are not case-sensitive, so
ONCLICK works as well as
onClick and
onCLICK… But usually attributes are lowercased:
onclick.
We can assign a handler using a DOM property
on<event>.
For instance,
elem.onclick:
html autorun <input id="elem" type="button" value="Click me"> <script> *!* elem.onclick = function() { alert('Thank you'); }; */!* </script>
If the handler is assigned using an HTML-attribute then the browser reads it, creates a new function from the attribute content and writes it to the DOM property.
So this way is actually the same as the previous one.
These two code pieces work the same:
Only HTML:
html autorun height=50 <input type="button" *!*onclick="alert('Click!')"*/!* value="Button">
HTML + JS:
html autorun height=50 <input type="button" id="button" value="Button"> <script> *!* button.onclick = function() { alert('Click!'); }; */!* </script>
In the first example, the HTML attribute is used to initialize the
button.onclick, while in the second example - the script, that's all the difference.
As there's only one
onclick property, we can't assign more than one event handler.
In the example below adding a handler with JavaScript overwrites the existing handler:
html run height=50 autorun <input type="button" id="elem" onclick="alert('Before')" value="Click me"> <script> *!* elem.onclick = function() { // overwrites the existing handler alert('After'); // only this will be shown }; */!* </script>
To remove a handler - assign
elem.onclick = null.
The value of
this inside a handler is the element. The one which has the handler on it.
In the code below
button shows its contents using
this.innerHTML:
html height=50 autorun <button onclick="alert(this.innerHTML)">Click me</button>
If you're starting to work with events - please note some subtleties.
We can set an existing function as a handler:
But be careful: the function should be assigned as
sayThanks, not
sayThanks().
If we add parentheses, then
sayThanks() becomes is a function call. So the last line actually takes the
result of the function execution, that is
undefined (as the function returns nothing), and assigns it to
onclick. That doesn't work.
…On the other hand, in the markup we do need the parentheses:
The difference is easy to explain. When the browser reads the attribute, it creates a handler function with body from the attribute content.
So the markup generates this property:
Don't use
setAttribute for handlers.
Such a call won't work:
js run no-beautify // a click on <body> will generate errors, // because attributes are always strings, function becomes a string document.body.setAttribute('onclick', function() { alert(1) });
DOM-property case matters.
Assign a handler to
elem.onclick, not
elem.ONCLICK, because DOM properties are case-sensitive.
The fundamental problem of the aforementioned ways to assign handlers - we can't assign multiple handlers to one event.
Let's say, one part of our code wants to highlight a button on click, and another one wants to show a message on the same click.
We'd like to assign two event handlers for that. But a new DOM property will overwrite the existing one:
js no-beautify input.onclick = function() { alert(1); } // ... input.onclick = function() { alert(2); } // replaces the previous handler
Developers of web standards understood that long ago and suggested an alternative way of managing handlers using
special methods
addEventListener and
removeEventListener. They are free of such a problem.
The syntax to add a handler:
event
"click".
handler
options
once: if
true, then the listener is automatically removed after it triggers. -
capture: the phase where to handle the event, to be covered later in the chapter
info:bubbling-and-capturing. For historical reasons,
options can also be
false/true, that's the same as
{capture: false/true}. -
passive: if
true, then the handler will not call
preventDefault(), we'll explain that later in
info:default-browser-action.
To remove the handler, use
removeEventListener:
warn header="Removal requires the same function" To remove a handler we should pass exactly the same function as was assigned.
This doesn't work:
js no-beautify elem.addEventListener( "click" , () => alert('Thanks!')); // .... elem.removeEventListener( "click", () => alert('Thanks!'));
The handler won't be removed, because
removeEventListener gets another function - with the same code, but that doesn't matter, as it's a
different function object.
Here's the right way:
function
handler()
{
alert(
'Thanks!' )
;
}
input.
addEventListener(
"click"
, handler)
;
// ....
input.
removeEventListener(
"click"
, handler)
;
Please note - if we don't store the function in a variable, then we can't remove it. There's no way to "read
back" handlers assigned by
addEventListener.
Multiple calls to
addEventListener allow to add multiple handlers, like this:
run no-beautify
As we can see in the example above, we can set handlers
both using a DOM-property and
addEventListener. But generally we use only one of these ways.
warn header="For some events, handlers only work withaddEventListener
" There exist events that can't be assigned via a DOM-property. Only withaddEventListener`.
For instance, the
DOMContentLoaded event, that triggers when the document is loaded and DOM is built.
// this way it works
document.
addEventListener(
"DOMContentLoaded"
,
function()
{
alert(
"DOM built")
;
})
;
So
addEventListener is more universal. Although, such events are an exception rather than the rule.
To properly handle an event we'd want to know more about what's happened. Not just a "click" or a "keydown", but what were the pointer coordinates? Which key was pressed? And so on.
When an event happens, the browser creates an event object, puts details into it and passes it as an argument to the handler.
Here's an example of getting pointer coordinates from the event object:
run
Some properties of
event object:
event.type
"click".
event.currentTarget
this, unless the handler is an arrow function, or its
this is bound to something else, then we can get the element from
event.currentTarget.
event.clientX / event.clientY
There are more properties. Many of them depend on the event type: keyboard events have one set of properties, pointer events - another one, we'll study them later when we come to different events in details.
smart header="The event object is also available in HTML handlers" If we assign a handler in HTML, we can also use theevent`
object, like this:
html autorun height=60 <input type="button" onclick="*!*alert(event.type)*/!*" value="Event type">
That's possible because when the browser reads the attribute, it creates a handler like this:
function(event) { alert(event.type) }. That is: its first argument is called
"event", and the body is taken from the attribute.
We can assign not just a function, but an object as an event handler using
addEventListener. When an event occurs, its
handleEvent method is called.
For instance:
run
As we can see, when
addEventListener receives an object as the handler, it calls
obj.handleEvent(event) in case of an event.
We could also use a class for that:
run
Here the same object handles both events. Please note that we need to explicitly setup the events to listen using
addEventListener. The
menu object only gets
mousedown and
mouseup here, not any other types of events.
The method
handleEvent does not have to do all the job by itself. It can call other event-specific methods
instead, like this:
run
Now event handlers are clearly separated, that may be easier to support.
There are 3 ways to assign event handlers:
onclick="...".
elem.onclick = function.
elem.addEventListener(event, handler[, phase]) to add,
removeEventListener to remove.
HTML attributes are used sparingly, because JavaScript in the middle of an HTML tag looks a little bit odd and alien. Also can't write lots of code in there.
DOM properties are ok to use, but we can't assign more than one handler of the particular event. In many cases that limitation is not pressing.
The last way is the most flexible, but it is also the longest to write. There are few events that only work with
it, for instance
transitionend and
DOMContentLoaded (to be covered). Also
addEventListener supports objects as event handlers. In that case the method
handleEvent is called in case of the event.
No matter how you assign the handler - it gets an event object as the first argument. That object contains the details about what's happened.
We'll learn more about events in general and about different types of events in the next chapters.
Let's start with an example.
This handler is assigned to
<div>, but also runs if you click any nested tag like
<em> or
<code>:
html autorun height=60 <div onclick="alert('The handler!')"> <em>If you click on <code>EM</code>, the handler on <code>DIV</code> runs.</em> </div>
Isn't it a bit strange? Why does the handler on
<div> run if the actual click was on
<em>?
The bubbling principle is simple.
When an event happens on an element, it first runs the handlers on it, then on its parent, then all the way up on other ancestors.
Let's say we have 3 nested elements
FORM > DIV > P with a handler on each of them:
A click on the inner
<p> first runs
onclick: 1. On that
<p>. 2. Then on the outer
<div>. 3. Then on the outer
<form>. 4. And so on upwards till the
document object.
So if we click on
<p>, then we'll see 3 alerts:
p ->
div ->
form.
The process is called "bubbling", because events "bubble" from the inner element up through parents like a bubble in the water.
warn header=" Almost all events bubble." The key word in this phrase is "almost".
For instance, a
focus event does not bubble. There are other examples too, we'll meet them. But still it's an
exception, rather than a rule, most events do bubble.
A handler on a parent element can always get the details about where it actually happened.
The most deeply nested element that caused the event is called a
target element, accessible as
event.target.
Note the differences from
this (=
event.currentTarget):
event.target - is the "target" element that initiated the event, it doesn't change through the
bubbling process.
this - is the "current" element, the one that has a currently running handler on it.
For instance, if we have a single handler
form.onclick, then it can "catch" all clicks inside the form. No matter where the click happened, it
bubbles up to
<form> and runs the handler.
In
form.onclick handler:
this (=
event.currentTarget) is the
<form> element, because the handler runs on it.
event.target is the actual element inside the form that was clicked.
Check it out:
[codetabs height=220 src="bubble-target"]
It's possible that
event.target could equal
this - it happens when the click is made directly on the
<form> element.
A bubbling event goes from the target element straight up. Normally it goes upwards till
<html>, and then to
document object, and some events even reach
window, calling all handlers on the path.
But any handler may decide that the event has been fully processed and stop the bubbling.
The method for it is
event.stopPropagation().
For instance, here
body.onclick doesn't work if you click on
<button>:
html run autorun height=60 <body onclick="alert(`the bubbling doesn't reach here`)"> <button onclick="event.stopPropagation()">Click me</button> </body>
smart header="event.stopImmediatePropagation()" If an element has multiple event handlers on a single event, then even if one of them stops the bubbling, the other ones still execute.
In other words,
event.stopPropagation() stops the move upwards, but on the current element all other handlers will
run.
To stop the bubbling and prevent handlers on the current element from running, there's a method
event.stopImmediatePropagation(). After it no other handlers execute.
warn header="Don't stop bubbling without a need!" Bubbling is convenient. Don't stop it without a real need: obvious and architecturally well thought out.
Sometimes
event.stopPropagation() creates hidden pitfalls that later may become problems.
For instance:
stopPropagation so that the outer menu won't trigger.
document.addEventListener('click'…) to catch all clicks.
stopPropagation. Sadly, we've got a "dead zone".
There's usually no real need to prevent the bubbling. A task that seemingly requires that may be solved by other
means. One of them is to use custom events, we'll cover them later. Also we can write our data into the
event object in one handler and read it in another one, so we can pass to handlers on parents
information about the processing below.
There's another phase of event processing called "capturing". It is rarely used in real code, but sometimes can be useful.
The standard DOM Events describes 3 phases of event propagation:
Here's the picture of a click on
<td> inside a table, taken from the specification:
That is: for a click on
<td> the event first goes through the ancestors chain down to the element (capturing phase),
then it reaches the target and triggers there (target phase), and then it goes up (bubbling phase), calling
handlers on its way.
Before we only talked about bubbling, because the capturing phase is rarely used. Normally it is invisible to us.
Handlers added using
on<event>-property or using HTML attributes or using two-argument
addEventListener(event, handler) don't know anything about capturing, they only run on the 2nd and
3rd phases.
To catch an event on the capturing phase, we need to set the handler
capture option to
true:
elem.
addEventListener(...
,
{
capture
:
true
})
// or, just "true" is an alias to {capture: true}
elem.
addEventListener(...
,
true)
There are two possible values of the
capture option:
false (default), then the handler is set on the bubbling phase.
true, then the handler is set on the capturing phase.
Note that while formally there are 3 phases, the 2nd phase ("target phase": the event reached the element) is not handled separately: handlers on both capturing and bubbling phases trigger at that phase.
Let's see both capturing and bubbling in action:
run autorun height=140 edit
The code sets click handlers on every element in the document to see which ones are working.
If you click on
<p>, then the sequence is:
HTML ->
BODY ->
FORM ->
DIV (capturing phase, the first listener):
P (target phase, triggers two times, as we've set two listeners: capturing and bubbling)
DIV ->
FORM ->
BODY ->
HTML (bubbling phase, the second listener).
There's a property
event.eventPhase that tells us the number of the phase on which the event was caught. But it's rarely
used, because we usually know it in the handler.
smart header="To remove the handler, `removeEventListener` needs the same phase" If we `addEventListener(..., true)`, then we should mention the same phase in `removeEventListener(..., true)` to correctly remove the handler.
smart header="Listeners on same element and same phase run in their set order" If we have multiple event handlers on the same phase, assigned to the same element withaddEventListener`,
they run in the same order as they are created:
elem.
addEventListener(
"click"
, e
=>
alert(
1))
;
// guaranteed to trigger first
elem.
addEventListener(
"click"
, e
=>
alert(
2))
;
When an event happens - the most nested element where it happens gets labeled as the "target element" (
event.target).
event.target, calling handlers assigned with
addEventListener(..., true) on the way (
true is a shorthand for
{capture: true}).
event.target to the root, calling handlers assigned using
on<event>, HTML attributes and
addEventListener without the 3rd argument or with the 3rd argument
false/{capture:false}.
Each handler can access
event object properties:
event.target - the deepest element that originated the event.
event.currentTarget (=
this) - the current element that handles the event (the one that has the handler on it)
event.eventPhase - the current phase (capturing=1, target=2, bubbling=3).
Any event handler can stop the event by calling
event.stopPropagation(), but that's not recommended, because we can't really be sure we won't need it
above, maybe for completely different things.
The capturing phase is used very rarely, usually we handle events on bubbling. And there's a logic behind that.
In real world, when an accident happens, local authorities react first. They know best the area where it happened. Then higher-level authorities if needed.
The same for event handlers. The code that set the handler on a particular element knows maximum details about
the element and what it does. A handler on a particular
<td> may be suited for that exactly
<td>, it knows everything about it, so it should get the chance first. Then its immediate
parent also knows about the context, but a little bit less, and so on till the very top element that handles
general concepts and runs the last one.
Bubbling and capturing lay the foundation for "event delegation" - an extremely powerful event handling pattern that we study in the next chapter.
Capturing and bubbling allow us to implement one of most powerful event handling patterns called event delegation.
The idea is that if we have a lot of elements handled in a similar way, then instead of assigning a handler to each of them - we put a single handler on their common ancestor.
In the handler we get
event.target to see where the event actually happened and handle it.
Let's see an example - the Ba-Gua diagram reflecting the ancient Chinese philosophy.
Here it is:
[iframe height=350 src="bagua" edit link]
The HTML is like this:
<table>
<tr>
<th
colspan=
"3"
><em>Bagua
</em> Chart: Direction, Element, Color, Meaning
</th>
</tr>
<tr>
<td
class=
"nw"
><strong>Northwest
</strong><br>Metal
<br>Silver
<br>Elders
</td>
<td
class=
"n"
>...
</td>
<td
class=
"ne"
>...
</td>
</tr>
<tr>...2 more lines of this kind...
</tr>
<tr>...2 more lines of this kind...
</tr>
</table>
The table has 9 cells, but there could be 99 or 9999, doesn't matter.
Our task is to highlight a cell
<td> on click.
Instead of assign an
onclick handler to each
<td> (can be many) - we'll setup the "catch-all" handler on
<table> element.
It will use
event.target to get the clicked element and highlight it.
The code:
let selectedTd
;
*!*
table.
onclick
=
function(event)
{
let target
=
event.
target
;
// where was the click?
if (
target.
tagName
!=
'TD')
return
;
// not on TD? Then we're not interested
highlight(target)
;
// highlight it
};
*
/!
*
function highlight
(
td
)
{
if
(
selectedTd
)
{ // remove the existing highlight if any
selectedTd.classList.remove
(
'highlight'
)
;
}
selectedTd = td;
selectedTd.classList.add
(
'highlight'
)
; // highlight the new td
}
Such a code doesn't care how many cells there are in the table. We can add/remove
<td> dynamically at any time and the highlighting will still work.
Still, there's a drawback.
The click may occur not on the
<td>, but inside it.
In our case if we take a look inside the HTML, we can see nested tags inside
<td>, like
<strong>:
Naturally, if a click happens on that
<strong> then it becomes the value of
event.target.
In the handler
table.onclick we should take such
event.target and find out whether the click was inside
<td> or not.
Here's the improved code:
table.
onclick
=
function(event)
{
let td
=
event.
target.
closest(
'td')
;
// (1)
if (
!td)
return
;
// (2)
if (
!
table.
contains(td))
return
;
// (3)
highlight(td)
;
// (4)
};
Explanations: 1. The method
elem.closest(selector) returns the nearest ancestor that matches the selector. In our case we look
for
<td> on the way up from the source element. 2. If
event.target is not inside any
<td>, then the call returns immediately, as there's nothing to do. 3. In case of nested tables,
event.target may be a
<td>, but lying outside of the current table. So we check if that's actually
our table's
<td>. 4. And, if it's so, then highlight it.
As the result, we have a fast, efficient highlighting code, that doesn't care about the total number of
<td> in the table.
There are other uses for event delegation.
Let's say, we want to make a menu with buttons "Save", "Load", "Search" and so on. And there's an object with
methods
save,
load,
search… How to match them?
The first idea may be to assign a separate handler to each button. But there's a more elegant solution. We can
add a handler for the whole menu and
data-action attributes for buttons that has the method to call:
The handler reads the attribute and executes the method. Take a look at the working example:
autorun height=60 run untrusted
Please note that
this.onClick is bound to
this in
(*). That's important, because otherwise
this inside it would reference the DOM element (
elem), not the
Menu object, and
this[action] would not be what we need.
So, what advantages does delegation give us here?
+ We don't need to write the code to assign a handler to each button. Just make a method and put it in the markup.
+ The HTML structure is flexible, we can add/remove buttons at any time.
We could also use classes
.action-save,
.action-load, but an attribute
data-action is better semantically. And we can use it in CSS rules too.
We can also use event delegation to add "behaviors" to elements declaratively, with special attributes and classes.
The pattern has two parts: 1. We add a custom attribute to an element that describes its behavior. 2. A document-wide handler tracks events, and if an event happens on an attributed element - performs the action.
For instance, here the attribute
data-counter adds a behavior: "increase value on click" to buttons:
run autorun height=60 Counter: One more counter:
If we click a button - its value is increased. Not buttons, but the general approach is important here.
There can be as many attributes with
data-counter as we want. We can add new ones to HTML at any moment. Using the event delegation we
"extended" HTML, added an attribute that describes a new behavior.
``
warn header="For document-level handlers -- alwaysaddEventListener
" When we assign an event handler to thedocument
object, we should always useaddEventListener
, notdocument.on
For real projects it's normal that there are many handlers on
document set by different parts of the code.
One more example of behavior. A click on an element with the attribute
data-toggle-id will show/hide the element with the given
id:
autorun run height=60 <button !data-toggle-id="subscribe-mail" /!> Show the subscription form
Let's note once again what we did. Now, to add toggling functionality to an element - there's no need to know
JavaScript, just use the attribute
data-toggle-id.
That may become really convenient - no need to write JavaScript for every such element. Just use the behavior. The document-level handler makes it work for any element of the page.
We can combine multiple behaviors on a single element as well.
The "behavior" pattern can be an alternative to mini-fragments of JavaScript.
Event delegation is really cool! It's one of the most helpful patterns for DOM events.
It's often used to add the same handling for many similar elements, but not only for that.
The algorithm:
event.target.
Benefits:
+ Simplifies initialization and saves memory: no need to add many handlers.
+ Less code: when adding or removing elements, no need to add/remove handlers.
+ DOM modifications: we can mass add/remove elements with `innerHTML` and the like.
The delegation has its limitations of course:
- First, the event must be bubbling. Some events do not bubble. Also, low-level handlers should not use `event.stopPropagation()`.
- Second, the delegation may add CPU load, because the container-level handler reacts on events in any place of the container, no matter whether they interest us or not. But usually the load is negligible, so we don't take it into account.
Many events automatically lead to certain actions performed by the browser.
For instance:
If we handle an event in JavaScript, we may not want the corresponding browser action to happen, and want to implement another behavior instead.
There are two ways to tell the browser we don't want it to act:
event object. There's a method
event.preventDefault().
on<event> (not by
addEventListener), then returning
false also works the same.
In this HTML a click on a link doesn't lead to navigation, browser doesn't do anything:
html autorun height=60 no-beautify <a href="/" onclick="return false">Click here</a> or <a href="/" onclick="event.preventDefault()">here</a>
In the next example we'll use this technique to create a JavaScript-powered menu.
``
warn header="Returningfalse` from a handler is an exception" The value returned by an event handler
is usually ignored.
The only exception is
return false from a handler assigned using
on<event>.
In all other cases,
return value is ignored. In particular, there's no sense in returning
true.
Consider a site menu, like this:
<ul
id=
"menu"
class=
"menu"
>
<li><a
href=
"/html"
>HTML
</a></li>
<li><a
href=
"/javascript"
>JavaScript
</a></li>
<li><a
href=
"/css"
>CSS
</a></li>
</ul>
Here's how it looks with some CSS:
[iframe height=70 src="menu" link edit]
Menu items are implemented as HTML-links
<a>, not buttons
<button>. There are several reasons to do so, for instance:
<button> or
<span>, that doesn't work.
<a href="..."> links while indexing.
So we use
<a> in the markup. But normally we intend to handle clicks in JavaScript. So we should prevent
the default browser action.
Like here:
menu.
onclick
=
function(event)
{
if (
event.
target.
nodeName
!=
'A')
return
;
let href
=
event.
target.
getAttribute(
'href')
;
alert( href )
;
// ...can be loading from the server, UI generation etc
*!*
return
false
;
// prevent browser action (don't go to the URL)
*
/!
*
};
If we omit
return false, then after our code executes the browser will do its "default action" - navigating to
the URL in
href. And we don't need that here, as we're handling the click by ourselves.
By the way, using event delegation here makes our menu very flexible. We can add nested lists and style them using CSS to "slide down".
smart header="Follow-up events" Certain events flow one into another. If we prevent the first event, there will be no second.
For instance,
mousedown on an
<input> field leads to focusing in it, and the
focus event. If we prevent the
mousedown event, there's no focus.
Try to click on the first
<input> below - the
focus event happens. But if you click the second one, there's no focus.
html run autorun <input value="Focus works" onfocus="this.value=''"> <input *!*onmousedown="return false"*/!* onfocus="this.value=''" value="Click me">
That's because the browser action is canceled on
mousedown. The focusing is still possible if we use another way to enter the input. For instance, the
key:Tab key to switch from the 1st input into the 2nd. But not with the mouse click any more.
The optional
passive: true option of
addEventListener signals the browser that the handler is not going to call
preventDefault().
Why that may be needed?
There are some events like
touchmove on mobile devices (when the user moves their finger across the screen), that cause
scrolling by default, but that scrolling can be prevented using
preventDefault() in the handler.
So when the browser detects such event, it has first to process all handlers, and then if
preventDefault is not called anywhere, it can proceed with scrolling. That may cause unnecessary
delays and "jitters" in the UI.
The
passive: true options tells the browser that the handler is not going to cancel scrolling. Then
browser scrolls immediately providing a maximally fluent experience, and the event is handled by the way.
For some browsers (Firefox, Chrome),
passive is
true by default for
touchstart and
touchmove events.
The property
event.defaultPrevented is
true if the default action was prevented, and
false otherwise.
There's an interesting use case for it.
You remember in the chapter
info:bubbling-and-capturing we talked about
event.stopPropagation() and why stopping bubbling is bad?
Sometimes we can use
event.defaultPrevented instead, to signal other event handlers that the event was handled.
Let's see a practical example.
By default the browser on
contextmenu event (right mouse click) shows a context menu with standard options. We can prevent it
and show our own, like this:
autorun height=50 no-beautify run
<button !oncontextmenu="alert(‘Draw our menu'); return false" /!> Right-click shows our context menu
Now, in addition to that context menu we'd like to implement document-wide context menu.
Upon right click, the closest context menu should show up.
autorun height=80 no-beautify runRight-click here for the document context menu
The problem is that when we click on
elem, we get two menus: the button-level and (the event bubbles up) the document-level menu.
How to fix it? One of solutions is to think like: "When we handle right-click in the button handler, let's stop
its bubbling" and use
event.stopPropagation():
Right-click for the document menu
Now the button-level menu works as intended. But the price is high. We forever deny access to information about right-clicks for any outer code, including counters that gather statistics and so on. That's quite unwise.
An alternative solution would be to check in the
document handler if the default action was prevented? If it is so, then the event was handled, and we
don't need to react on it.
Right-click for the document menu (added a check for event.defaultPrevented)
Now everything also works correctly. If we have nested elements, and each of them has a context menu of its own,
that would also work. Just make sure to check for
event.defaultPrevented in each
contextmenu handler.
smart header="event.stopPropagation() and event.preventDefault()" As we can clearly see, `event.stopPropagation()` and `event.preventDefault()` (also known as `return false`) are two different things. They are not related to each other.
``
smart header="Nested context menus architecture" There are also alternative ways to implement nested context menus. One of them is to have a single global object with a handler fordocument.oncontextmenu`,
and also methods that allow us to store other handlers in it.
The object will catch any right-click, look through stored handlers and run the appropriate one.
But then each piece of code that wants a context menu should know about that object and use its help instead of
the own
contextmenu handler.
There are many default browser actions:
mousedown - starts the selection (move the mouse to select).
click on
<input type="checkbox"> - checks/unchecks the
input.
submit - clicking an
<input type="submit"> or hitting
key:Enter inside a form field causes this event to happen, and the browser submits the form after
it.
keydown - pressing a key may lead to adding a character into a field, or other actions.
contextmenu - the event happens on a right-click, the action is to show the browser context menu.
All the default actions can be prevented if we want to handle the event exclusively by JavaScript.
To prevent a default action - use either
event.preventDefault() or
return false. The second method works only for handlers assigned with
on<event>.
The
passive: true option of
addEventListener tells the browser that the action is not going to be prevented. That's useful for
some mobile events, like
touchstart and
touchmove, to tell the browser that it should not wait for all handlers to finish before scrolling.
If the default action was prevented, the value of
event.defaultPrevented becomes
true, otherwise it's
false.
``
warn header="Stay semantic, don't abuse" Technically, by preventing default actions and adding JavaScript we can customize the behavior of any elements. For instance, we can make a link
work like a button, and a button
But we should generally keep the semantic meaning of HTML elements. For instance,
<a> should perform navigation, not a button.
Besides being "just a good thing", that makes your HTML better in terms of accessibility.
Also if we consider the example with
<a>, then please note: a browser allows us to open such links in a new window (by
right-clicking them and other means). And people like that. But if we make a button behave as a link using
JavaScript and even look like a link using CSS, then
<a>-specific browser features still won't work for it.
We can not only assign handlers, but also generate events from JavaScript.
Custom events can be used to create "graphical components". For instance, a root element of our own JS-based menu
may trigger events telling what happens with the menu:
open (menu open),
select (an item is selected) and so on. Another code may listen for the events and observe what's
happening with the menu.
We can generate not only completely new events, that we invent for our own purposes, but also built-in ones, such
as
click,
mousedown etc. That may be helpful for automated testing.
Built-in event classes form a hierarchy, similar to DOM element classes. The root is the built-in Event class.
We can create
Event objects like this:
Arguments:
"click" or our own like
"my-event".
bubbles: true/false - if
true, then the event bubbles.
cancelable: true/false - if
true, then the "default action" may be prevented. Later we'll see what it means for custom
events.
{bubbles: false, cancelable: false}.
After an event object is created, we should "run" it on an element using the call
elem.dispatchEvent(event).
Then handlers react on it as if it were a regular browser event. If the event was created with the
bubbles flag, then it bubbles.
In the example below the
click event is initiated in JavaScript. The handler works same way as if the button was clicked:
run no-beautify
smart header="event.isTrusted" There is a way to tell a "real" user event from a script-generated one.
The property
event.isTrusted is
true for events that come from real user actions and
false for script-generated events.
We can create a bubbling event with the name
"hello" and catch it on
document.
All we need is to set
bubbles to
true:
Notes:
addEventListener for our custom events, because
on<event> only exists for built-in events,
document.onhello doesn't work.
bubbles:true, otherwise the event won't bubble up.
The bubbling mechanics is the same for built-in (
click) and custom (
hello) events. There are also capturing and bubbling stages.
Here's a short list of classes for UI Events from the UI Event specification:
UIEvent
FocusEvent
MouseEvent
WheelEvent
KeyboardEvent
We should use them instead of
new Event if we want to create such events. For instance,
new MouseEvent("click").
The right constructor allows to specify standard properties for that type of event.
Like
clientX/clientY for a mouse event:
run let event = new MouseEvent("click", { bubbles: true, cancelable: true, clientX: 100, clientY: 100 });
! alert(event.clientX); // 100 /!
Please note: the generic
Event constructor does not allow that.
Let's try:
run let event = new Event("click", { bubbles: true, // only bubbles and cancelable cancelable: true, // work in the Event constructor clientX: 100, clientY: 100 });
! alert(event.clientX); // undefined, the unknown property is ignored! /!
Technically, we can work around that by assigning directly
event.clientX=100 after creation. So that's a matter of convenience and following the rules.
Browser-generated events always have the right type.
The full list of properties for different UI events is in the specification, for instance, MouseEvent.
For our own, completely new events types like
"hello" we should use
new CustomEvent. Technically
CustomEvent is the same as
Event, with one exception.
In the second argument (object) we can add an additional property
detail for any custom information that we want to pass with the event.
For instance:
run refresh
The
detail property can have any data. Technically we could live without, because we can assign any
properties into a regular
new Event object after its creation. But
CustomEvent provides the special
detail field for it to evade conflicts with other event properties.
Besides, the event class describes "what kind of event" it is, and if the event is custom, then we should use
CustomEvent just to be clear about what it is.
Many browser events have a "default action", such as navigating to a link, starting a selection, and so on.
For new, custom events, there are definitely no default browser actions, but a code that dispatches such event may have its own plans what to do after triggering the event.
By calling
event.preventDefault(), an event handler may send a signal that those actions should be canceled.
In that case the call to
elem.dispatchEvent(event) returns
false. And the code that dispatched it knows that it shouldn't continue.
Let's see a practical example - a hiding rabbit (could be a closing menu or something else).
Below you can see a
#rabbit and
hide() function that dispatches
"hide" event on it, to let all interested parties know that the rabbit is going to hide.
Any handler can listen for that event with
rabbit.addEventListener('hide',...) and, if needed, cancel the action using
event.preventDefault(). Then the rabbit won't disappear:
|\ /|
\|_|/
/. .\
=\_Y_/=
{>o<}
Please note: the event must have the flag
cancelable: true, otherwise the call
event.preventDefault() is ignored.
Usually events are processed in a queue. That is: if the browser is processing
onclick and a new event occurs, e.g. mouse moved, then it's handling is queued up, corresponding
mousemove handlers will be called after
onclick processing is finished.
The notable exception is when one event is initiated from within another one, e.g. using
dispatchEvent. Such events are processed immediately: the new event handlers are called, and then the
current event handling is resumed.
For instance, in the code below the
menu-open event is triggered during the
onclick.
It's processed immediately, without waiting for
onclick handler to end:
run autorun
The output order is: 1 -> nested -> 2.
Please note that the nested event
menu-open is caught on the
document. The propagation and handling of the nested event is finished before the processing gets
back to the outer code (
onclick).
That's not only about
dispatchEvent, there are other cases. If an event handler calls methods that trigger other events -
they are processed synchronously too, in a nested fashion.
Let's say we don't like it. We'd want
onclick to be fully processed first, independently from
menu-open or any other nested events.
Then we can either put the
dispatchEvent (or another event-triggering call) at the end of
onclick or, maybe better, wrap it in the zero-delay
setTimeout:
run
Now
dispatchEvent runs asynchronously after the current code execution is finished, including
menu.onclick, so event handlers are totally separate.
The output order becomes: 1 -> 2 -> nested.
To generate an event from code, we first need to create an event object.
The generic
Event(name, options) constructor accepts an arbitrary event name and the
options object with two properties: -
bubbles: true if the event should bubble. -
cancelable: true if the
event.preventDefault() should work.
Other constructors of native events like
MouseEvent,
KeyboardEvent and so on accept properties specific to that event type. For instance,
clientX for mouse events.
For custom events we should use
CustomEvent constructor. It has an additional option named
detail, we should assign the event-specific data to it. Then all handlers can access it as
event.detail.
Despite the technical possibility of generating browser events like
click or
keydown, we should use them with great care.
We shouldn't generate browser events as it's a hacky way to run handlers. That's bad architecture most of the time.
Native events might be generated:
Custom events with our own names are often generated for architectural purposes, to signal what happens inside our menus, sliders, carousels etc.
In this chapter we'll get into more details about mouse events and their properties.
Please note: such events may come not only from "mouse devices", but are also from other devices, such as phones and tablets, where they are emulated for compatibility.
We've already seen some of these events:
mousedown/mouseup
mouseover/mouseout
mousemove
click
mousedown and then
mouseup over the same element if the left mouse button was used.
dblclick
contextmenu
…There are several other events too, we'll cover them later.
As you can see from the list above, a user action may trigger multiple events.
For instance, a left-button click first triggers
mousedown, when the button is pressed, then
mouseup and
click when it's released.
In cases when a single action initiates multiple events, their order is fixed. That is, the handlers are called
in the order
mousedown ->
mouseup ->
click.
Click the button below and you'll see the events. Try double-click too.
On the teststand below all mouse events are logged, and if there is more than a 1 second delay between them they are separated by a horizontal ruler.
Also we can see the `button` property that allows to detect the mouse button, it's explained below.
<input onmousedown="return logMouse(event)" onmouseup="return logMouse(event)" onclick="return logMouse(event)" oncontextmenu="return logMouse(event)" ondblclick="return logMouse(event)" value="Click me with the right or the left mouse button" type="button"> <input onclick="logClear('test')" value="Clear" type="button"> <form id="testform" name="testform"> <textarea style="font-size:12px;height:150px;width:360px;"></textarea></form>
Click-related events always have the
button property, which allows to get the exact mouse button.
We usually don't use it for
click and
contextmenu events, because the former happens only on left-click, and the latter - only on
right-click.
From the other hand,
mousedown and
mouseup handlers may need
event.button, because these events trigger on any button, so
button allows to distinguish between "right-mousedown" and "left-mousedown".
The possible values of
event.button are:
| Button state |
event.button
|
|---|---|
| Left button (primary) | 0 |
| Middle button (auxiliary) | 1 |
| Right button (secondary) | 2 |
| X1 button (back) | 3 |
| X2 button (forward) | 4 |
Most mouse devices only have the left and right buttons, so possible values are
0 or
2. Touch devices also generate similar events when one taps on them.
Also there's
event.buttons property that has all currently pressed buttons as an integer, one bit per button. In
practice this property is very rarely used, you can find details at
MDN if you ever need it.
``
warn header="The outdatedevent.which
" Old code may useevent.which` property that's an old non-standard way of getting a button, with
possible values:
event.which == 1 - left button,
event.which == 2 - middle button,
event.which == 3 - right button.
As of now,
event.which is deprecated, we shouldn't use it.
All mouse events include the information about pressed modifier keys.
Event properties:
shiftKey:
key:Shift
altKey:
key:Alt (or
key:Opt for Mac)
ctrlKey:
key:Ctrl
metaKey:
key:Cmd for Mac
They are
true if the corresponding key was pressed during the event.
For instance, the button below only works on
key:Alt+Shift+click:
autorun height=60
``
warn header="Attention: on Mac it's usuallyCmd
instead ofCtrl
" On Windows and Linux there are modifier keyskey:Alt
,key:Shift
andkey:Ctrl
. On Mac there's one more:key:Cmd
, corresponding to the propertymetaKey`.
In most applications, when Windows/Linux uses
key:Ctrl, on Mac
key:Cmd is used.
That is: where a Windows user presses
key:Ctrl+Enter or
key:Ctrl+A, a Mac user would press
key:Cmd+Enter or
key:Cmd+A, and so on.
So if we want to support combinations like
key:Ctrl+click, then for Mac it makes sense to use
key:Cmd+click. That's more comfortable for Mac users.
Even if we'd like to force Mac users to
key:Ctrl+click - that's kind of difficult. The problem is: a left-click with
key:Ctrl is interpreted as a
right-click on MacOS, and it generates the
contextmenu event, not
click like Windows/Linux.
So if we want users of all operating systems to feel comfortable, then together with
ctrlKey we should check
metaKey.
For JS-code it means that we should check
if (event.ctrlKey || event.metaKey).
warn header="There are also mobile devices" Keyboard combinations are good as an addition to the workflow. So that if the visitor uses a keyboard - they work.
But if their device doesn't have it - then there should be a way to live without modifier keys.
All mouse events provide coordinates in two flavours:
clientX and
clientY.
pageX and
pageY.
We already covered the difference between them in the chapter info:coordinates.
In short, document-relative coordinates
pageX/Y are counted from the left-upper corner of the document, and do not change when the page is
scrolled, while
clientX/Y are counted from the current window left-upper corner. When the page is scrolled, they
change.
For instance, if we have a window of the size 500x500, and the mouse is in the left-upper corner, then
clientX and
clientY are
0, no matter how the page is scrolled.
And if the mouse is in the center, then
clientX and
clientY are
250, no matter what place in the document it is. They are similar to
position:fixed in that aspect.
Move the mouse over the input field to see `clientX/clientY` (the example is in the `iframe`, so coordinates are relative to that `iframe`):
autorun height=50
<input onmousemove="this.value=event.clientX+':'+event.clientY" value="Mouse over me">
Double mouse click has a side-effect that may be disturbing in some interfaces: it selects text.
For instance, double-clicking on the text below selects it in addition to our handler:
html autorun height=50 <span ondblclick="alert('dblclick')">Double-click me</span>
If one presses the left mouse button and, without releasing it, moves the mouse, that also makes the selection, often unwanted.
There are multiple ways to prevent the selection, that you can read in the chapter info:selection-range.
In this particular case the most reasonable way is to prevent the browser action on
mousedown. It prevents both these selections:
html autorun height=50 Before... <b ondblclick="alert('Click!')" *!*onmousedown="return false"*/!*> Double-click me </b> ...After
Now the bold element is not selected on double clicks, and pressing the left button on it won't start the selection.
Please note: the text inside it is still selectable. However, the selection should start not on the text itself, but before or after it. Usually that's fine for users.
smart header="Preventing copying" If we want to disable selection to protect our page content from copy-pasting, then we can use another event:oncopy`.
html autorun height=80 no-beautify <div *!*oncopy="alert('Copying forbidden!');return false"*/!*> Dear user, The copying is forbidden for you. If you know JS or HTML, then you can get everything from the page source though. </div>
If you try to copy a piece of text in the
<div>, that won't work, because the default action
oncopy is prevented.
Surely the user has access to HTML-source of the page, and can take the content from there, but not everyone knows how to do it.
Mouse events have the following properties:
button.
true if pressed):
altKey,
ctrlKey,
shiftKey and
metaKey (Mac).
key:Ctrl, then don't forget Mac users, they usually use
key:Cmd, so it's better to check
if (e.metaKey || e.ctrlKey).
clientX/clientY.
pageX/pageY.
The default browser action of
mousedown is text selection, if it's not good for the interface, then it should be prevented.
In the next chapter we'll see more details about events that follow pointer movement and how to track element changes under it.
Most of the time, operators and functions automatically convert the values given to them to the right type.
For example,
alert automatically converts any value to a string to show it. Mathematical operations convert values
to numbers.
There are also cases when we need to explicitly convert a value to the expected type.
smart header="Not talking about objects yet" In this chapter, we won't cover objects. For now we'll just be talking about primitives.
Later, after we learn about objects, in the chapter info:object-toprimitive we'll see how objects fit in.
String conversion happens when we need the string form of a value.
For example,
alert(value) does it to show the value.
We can also call the
String(value) function to convert a value to a string:
run let value = true; alert(typeof value); // boolean
! value = String(value); // now value is a string "true" alert(typeof value); // string /!
String conversion is mostly obvious. A
false becomes
"false",
null becomes
"null", etc.
Numeric conversion happens in mathematical functions and expressions automatically.
For example, when division
/ is applied to non-numbers:
js run alert( "6" / "2" ); // 3, strings are converted to numbers
We can use the
Number(value) function to explicitly convert a
value to a number:
run let str = "123"; alert(typeof str); // string
let num = Number(str); // becomes a number 123
alert(typeof num); // number
Explicit conversion is usually required when we read a value from a string-based source like a text form but expect a number to be entered.
If the string is not a valid number, the result of such a conversion is
NaN. For instance:
run let age = Number("an arbitrary string instead of a number");
alert(age); // NaN, conversion failed
Numeric conversion rules:
| Value | Becomes… |
|---|---|
undefined
|
NaN
|
null
|
0
|
true and false
|
1 and
0
|
string
|
Whitespaces from the start and end are removed. If the remaining string is empty, the result is
0. Otherwise, the number is "read" from the string. An error gives
NaN.
|
Examples:
js run alert( Number(" 123 ") ); // 123 alert( Number("123z") ); // NaN (error reading a number at "z") alert( Number(true) ); // 1 alert( Number(false) ); // 0
Please note that
null and
undefined behave differently here:
null becomes zero while
undefined becomes
NaN.
Most mathematical operators also perform such conversion, we'll see that in the next chapter.
Boolean conversion is the simplest one.
It happens in logical operations (later we'll meet condition tests and other similar things) but can also be
performed explicitly with a call to
Boolean(value).
The conversion rule:
0, an empty string,
null,
undefined, and
NaN, become
false.
true.
For instance:
run alert( Boolean(1) ); // true alert( Boolean(0) ); // false
alert( Boolean("hello") ); // true alert( Boolean("") ); // false
warn header="Please note: the string with zero"0"
istrue
" Some languages (namely PHP) treat"0"
asfalse
. But in JavaScript, a non-empty string is alwaystrue`.
js run alert( Boolean("0") ); // true alert( Boolean(" ") ); // spaces, also true (any non-empty string is true)
The three most widely used type conversions are to string, to number, and to boolean.
String Conversion
- Occurs when we output something. Can be performed with
String(value). The conversion to string is usually obvious for primitive values.
Numeric Conversion
- Occurs in math operations. Can be performed with
Number(value).
The conversion follows the rules:
| Value | Becomes… |
|---|---|
undefined
|
NaN
|
null
|
0
|
true / false
|
1 / 0
|
string
|
The string is read "as is", whitespaces from both sides are ignored. An empty string becomes
0. An error gives
NaN.
|
Boolean Conversion
- Occurs in logical operations. Can be performed with
Boolean(value).
Follows the rules:
| Value | Becomes… |
|---|---|
0,
null,
undefined,
NaN,
""
|
false
|
| any other value |
true
|
Most of these rules are easy to understand and memorize. The notable exceptions where people usually make mistakes are:
undefined is
NaN as a number, not
0.
"0" and space-only strings like
" " are true as a boolean.
Objects aren't covered here. We'll return to them later in the chapter info:object-toprimitive that is devoted exclusively to objects after we learn more basic things about JavaScript.
Let's dive into more details about events that happen when the mouse moves between elements.
The
mouseover event occurs when a mouse pointer comes over an element, and
mouseout - when it leaves.
These events are special, because they have property
relatedTarget. This property complements
target. When a mouse leaves one element for another, one of them becomes
target, and the other one -
relatedTarget.
For
mouseover:
event.target - is the element where the mouse came over.
event.relatedTarget - is the element from which the mouse came (
relatedTarget ->
target).
For
mouseout the reverse:
event.target - is the element that the mouse left.
event.relatedTarget - is the new under-the-pointer element, that mouse left for (
target ->
relatedTarget).
In the example below each face and its features are separate elements. When you move the mouse, you can see mouse events in the text area.
Each event has the information about both `target` and `relatedTarget`:
[codetabs src="mouseoverout" height=280]
``
warn header="relatedTarget
can benull
" TherelatedTarget
property can benull`.
That's normal and just means that the mouse came not from another element, but from out of the window. Or that it left the window.
We should keep that possibility in mind when using
event.relatedTarget in our code. If we access
event.relatedTarget.tagName, then there will be an error.
The
mousemove event triggers when the mouse moves. But that doesn't mean that every pixel leads to an
event.
The browser checks the mouse position from time to time. And if it notices changes then triggers the events.
That means that if the visitor is moving the mouse very fast then some DOM-elements may be skipped:
If the mouse moves very fast from
#FROM to
#TO elements as painted above, then intermediate
<div> elements (or some of them) may be skipped. The
mouseout event may trigger on
#FROM and then immediately
mouseover on
#TO.
That's good for performance, because there may be many intermediate elements. We don't really want to process in and out of each one.
On the other hand, we should keep in mind that the mouse pointer doesn't "visit" all elements along the way. It can "jump".
In particular, it's possible that the pointer jumps right inside the middle of the page from out of the window.
In that case
relatedTarget is
null, because it came from "nowhere":
You can check it out "live" on a teststand below.
Its HTML has two nested elements: the `<div id="child">` is inside the `<div id="parent">`. If you move the mouse fast over them, then maybe only the child div triggers events, or maybe the parent one, or maybe there will be no events at all.
Also move the pointer into the child `div`, and then move it out quickly down through the parent one. If the movement is fast enough, then the parent element is ignored. The mouse will cross the parent element without noticing it.
[codetabs height=360 src="mouseoverout-fast"]
smart header="If `mouseover` triggered, there must be `mouseout`" In case of fast mouse movements, intermediate elements may be ignored, but one thing we know for sure: if the pointer "officially" entered an element (`mouseover` event generated), then upon leaving it we always get `mouseout`.
An important feature of
mouseout - it triggers, when the pointer moves from an element to its descendant, e.g. from
#parent to
#child in this HTML:
If we're on
#parent and then move the pointer deeper into
#child, we get
mouseout on
#parent!
That may seem strange, but can be easily explained.
According to the browser logic, the mouse cursor may be only over a single element at any time - the most nested one and top by z-index.
So if it goes to another element (even a descendant), then it leaves the previous one.
Please note another important detail of event processing.
The
mouseover event on a descendant bubbles up. So, if
#parent has
mouseover handler, it triggers:
You can see that very well in the example below: `<div id="child">` is inside the `<div id="parent">`. There are `mouseover/out` handlers on `#parent` element that output event details.
If you move the mouse from `#parent` to `#child`, you see two events on `#parent`:
1. `mouseout [target: parent]` (left the parent), then
2. `mouseover [target: child]` (came to the child, bubbled).
[codetabs height=360 src="mouseoverout-child"]
As shown, when the pointer moves from
#parent element to
#child, two handlers trigger on the parent element:
mouseout and
mouseover:
parent.
onmouseout
=
function(event)
{
/* event.target: parent element */
};
parent.
onmouseover
=
function(event)
{
/* event.target: child element (bubbled) */
};
If we don't examine
event.target inside the handlers, then it may seem that the mouse pointer left
#parent element, and then immediately came back over it.
But that's not the case! The pointer is still over the parent, it just moved deeper into the child element.
If there are some actions upon leaving the parent element, e.g. an animation runs in
parent.onmouseout, we usually don't want it when the pointer just goes deeper into
#parent.
To avoid it, we can check
relatedTarget in the handler and, if the mouse is still inside the element, then ignore such event.
Alternatively we can use other events:
mouseenter and
mouseleave, that we'll be covering now, as they don't have such problems.
Events
mouseenter/mouseleave are like
mouseover/mouseout. They trigger when the mouse pointer enters/leaves the element.
But there are two important differences:
mouseenter/mouseleave do not bubble.
These events are extremely simple.
When the pointer enters an element -
mouseenter triggers. The exact location of the pointer inside the element or its descendants doesn't
matter.
When the pointer leaves an element -
mouseleave triggers.
This example is similar to the one above, but now the top element has `mouseenter/mouseleave` instead of `mouseover/mouseout`.
As you can see, the only generated events are the ones related to moving the pointer in and out of the top element. Nothing happens when the pointer goes to the child and back. Transitions between descendants are ignored
[codetabs height=340 src="mouseleave"]
Events
mouseenter/leave are very simple and easy to use. But they do not bubble. So we can't use event
delegation with them.
Imagine we want to handle mouse enter/leave for table cells. And there are hundreds of cells.
The natural solution would be - to set the handler on
<table> and process events there. But
mouseenter/leave don't bubble. So if such event happens on
<td>, then only a handler on that
<td> is able to catch it.
Handlers for
mouseenter/leave on
<table> only trigger when the pointer enters/leaves the table as a whole. It's impossible to
get any information about transitions inside it.
So, let's use
mouseover/mouseout.
Let's start with simple handlers that highlight the element under mouse:
// let's highlight an element under the pointer
table.
onmouseover
=
function(event)
{
let target
=
event.
target
;
target.
style.
background
=
'pink'
;
};
table.
onmouseout
=
function(event)
{
let target
=
event.
target
;
target.
style.
background
=
''
;
};
Here they are in action. As the mouse travels across the elements of this table, the current one is highlighted:
[codetabs height=480 src="mouseenter-mouseleave-delegation"]
In our case we'd like to handle transitions between table cells
<td>: entering a cell and leaving it. Other transitions, such as inside the cell or outside of
any cells, don't interest us. Let's filter them out.
Here's what we can do:
<td> in a variable, let's call it
currentElem.
mouseover - ignore the event if we're still inside the current
<td>.
mouseout - ignore if we didn't leave the current
<td>.
Here's an example of code that accounts for all possible situations:
[js src="mouseenter-mouseleave-delegation-2/script.js"]
Once again, the important features are: 1. It uses event delegation to handle entering/leaving of any
<td> inside the table. So it relies on
mouseover/out instead of
mouseenter/leave that don't bubble and hence allow no delegation. 2. Extra events, such as moving
between descendants of
<td> are filtered out, so that
onEnter/Leave runs only if the pointer leaves or enters
<td> as a whole.
Here's the full example with all details:
[codetabs height=460 src="mouseenter-mouseleave-delegation-2"]
Try to move the cursor in and out of table cells and inside them. Fast or slow -- doesn't matter. Only `<td>` as a whole is highlighted, unlike the example before.
We covered events
mouseover,
mouseout,
mousemove,
mouseenter and
mouseleave.
These things are good to note:
mouseover/out and
mouseenter/leave have an additional property:
relatedTarget. That's the element that we are coming from/to, complementary to
target.
Events
mouseover/out trigger even when we go from the parent element to a child element. The browser assumes
that the mouse can be only over one element at one time - the deepest one.
Events
mouseenter/leave are different in that aspect: they only trigger when the mouse comes in and out the
element as a whole. Also they do not bubble.
Drag'n'Drop is a great interface solution. Taking something and dragging and dropping it is a clear and simple way to do many things, from copying and moving documents (as in file managers) to ordering (dropping items into a cart).
In the modern HTML standard there's a
section about Drag and Drop with special
events such as
dragstart,
dragend, and so on.
These events allow us to support special kinds of drag'n'drop, such as handling dragging a file from OS file-manager and dropping it into the browser window. Then JavaScript can access the contents of such files.
But native Drag Events also have limitations. For instance, we can't prevent dragging from a certain area. Also we can't make the dragging "horizontal" or "vertical" only. And there are many other drag'n'drop tasks that can't be done using them. Also, mobile device support for such events is very weak.
So here we'll see how to implement Drag'n'Drop using mouse events.
The basic Drag'n'Drop algorithm looks like this:
mousedown - prepare the element for moving, if needed (maybe create a clone of it, add a class to
it or whatever).
mousemove move it by changing
left/top with
position:absolute.
mouseup - perform all actions related to finishing the drag'n'drop.
These are the basics. Later we'll see how to other features, such as highlighting current underlying elements while we drag over them.
Here's the implementation of dragging a ball:
ball.
onmousedown
=
function(event)
{
// (1) prepare to moving: make absolute and on top by z-index
ball.
style.
position
=
'absolute'
;
ball.
style.
zIndex
=
1000
;
// move it out of any current parents directly into body
// to make it positioned relative to the body
document.
body.
append(ball)
;
// centers the ball at (pageX, pageY) coordinates
function
moveAt(pageX
, pageY)
{
ball.
style.
left
= pageX
-
ball.
offsetWidth /
2
+
'px'
;
ball.
style.
top
= pageY
-
ball.
offsetHeight /
2
+
'px'
;
}
// move our absolutely positioned ball under the pointer
moveAt(
event.
pageX
,
event.
pageY)
;
function
onMouseMove(event)
{
moveAt(
event.
pageX
,
event.
pageY)
;
}
// (2) move the ball on mousemove
document.
addEventListener(
'mousemove'
, onMouseMove)
;
// (3) drop the ball, remove unneeded handlers
ball.
onmouseup
=
function()
{
document.
removeEventListener(
'mousemove'
, onMouseMove)
;
ball.
onmouseup
=
null
;
};
};
If we run the code, we can notice something strange. On the beginning of the drag'n'drop, the ball "forks": we start dragging its "clone".
Here's an example in action:
[iframe src="ball" height=230]
Try to drag'n'drop with the mouse and you'll see such behavior.
That's because the browser has its own drag'n'drop support for images and some other elements. It runs automatically and conflicts with ours.
To disable it:
Now everything will be all right.
In action:
[iframe src="ball2" height=230]
Another important aspect - we track
mousemove on
document, not on
ball. From the first sight it may seem that the mouse is always over the ball, and we can put
mousemove on it.
But as we remember,
mousemove triggers often, but not for every pixel. So after swift move the pointer can jump from the
ball somewhere in the middle of document (or even outside of the window).
So we should listen on
document to catch it.
In the examples above the ball is always moved so, that it's center is under the pointer:
ball.
style.
left
= pageX
-
ball.
offsetWidth /
2
+
'px'
;
ball.
style.
top
= pageY
-
ball.
offsetHeight /
2
+
'px'
;
Not bad, but there's a side-effect. To initiate the drag'n'drop, we can
mousedown anywhere on the ball. But if "take" it from its edge, then the ball suddenly "jumps" to
become centered under the mouse pointer.
It would be better if we keep the initial shift of the element relative to the pointer.
For instance, if we start dragging by the edge of the ball, then the pointer should remain over the edge while dragging.
Let's update our algorithm:
When a visitor presses the button (
mousedown) - remember the distance from the pointer to the left-upper corner of the ball in
variables
shiftX/shiftY. We'll keep that distance while dragging.
To get these shifts we can substract the coordinates:
Then while dragging we position the ball on the same shift relative to the pointer, like this:
The final code with better positioning:
ball.
onmousedown
=
function(event)
{
*!*
let shiftX
=
event.
clientX
-
ball.
getBoundingClientRect().
left
;
let shiftY
=
event.
clientY
-
ball.
getBoundingClientRect().
top
;
*
/!
*
ball.style.position = 'absolute';
ball.style.zIndex = 1000;
document.body.append
(
ball
)
;
moveAt
(
event.pageX, event.pageY
)
;
// moves the ball at
(
pageX, pageY
)
coordinates
// taking initial shifts into account
function moveAt
(
pageX, pageY
)
{
ball.style.left = pageX -
*
!
*
shiftX
*
/
!*
+
'px'
;
ball.
style.
top
= pageY
-
*!*shiftY
*
/!
*
+
'px';
}
function onMouseMove
(
event
)
{
moveAt
(
event.pageX, event.pageY
)
;
}
// move the ball on mousemove
document.addEventListener
(
'mousemove', onMouseMove
)
;
// drop the ball, remove unneeded handlers
ball.onmouseup = function
()
{
document.removeEventListener
(
'mousemove', onMouseMove
)
;
ball.onmouseup = null;
};
};
ball.ondragstart = function
()
{
return false;
};
In action (inside `<iframe>`):
[iframe src="ball3" height=230]
The difference is especially noticeable if we drag the ball by its right-bottom corner. In the previous example the ball "jumps" under the pointer. Now it fluently follows the pointer from the current position.
In previous examples the ball could be dropped just "anywhere" to stay. In real-life we usually take one element and drop it onto another. For instance, a "file" into a "folder" or something else.
Speaking abstract, we take a "draggable" element and drop it onto "droppable" element.
We need to know: - where the element was dropped at the end of Drag'n'Drop - to do the corresponding action, - and, preferably, know the droppable we're dragging over, to highlight it.
The solution is kind-of interesting and just a little bit tricky, so let's cover it here.
What may be the first idea? Probably to set
mouseover/mouseup handlers on potential droppables?
But that doesn't work.
The problem is that, while we're dragging, the draggable element is always above other elements. And mouse events only happen on the top element, not on those below it.
For instance, below are two
<div> elements, red one on top of the blue one (fully covers). There's no way to catch an event
on the blue one, because the red is on top:
html run autorun height=60 <style> div { width: 50px; height: 50px; position: absolute; top: 0; } </style> <div style="background:blue" onmouseover="alert('never works')"></div> <div style="background:red" onmouseover="alert('over red!')"></div>
The same with a draggable element. The ball is always on top over other elements, so events happen on it. Whatever handlers we set on lower elements, they won't work.
That's why the initial idea to put handlers on potential droppables doesn't work in practice. They won't run.
So, what to do?
There's a method called
document.elementFromPoint(clientX, clientY). It returns the most nested element on given
window-relative coordinates (or
null if given coordinates are out of the window).
We can use it in any of our mouse event handlers to detect the potential droppable under the pointer, like this:
// in a mouse event handler
ball.
hidden
=
true
;
// (*) hide the element that we drag
let elemBelow
=
document.
elementFromPoint(
event.
clientX
,
event.
clientY)
;
// elemBelow is the element below the ball, may be droppable
ball.
hidden
=
false
;
Please note: we need to hide the ball before the call
(*). Otherwise we'll usually have a ball on these coordinates, as it's the top element under the
pointer:
elemBelow=ball. So we hide it and immediately show again.
We can use that code to check what element we're "flying over" at any time. And handle the drop when it happens.
An extended code of
onMouseMove to find "droppable" elements:
// potential droppable that we're flying over right now
let currentDroppable
=
null
;
function
onMouseMove(event)
{
moveAt(
event.
pageX
,
event.
pageY)
;
ball.
hidden
=
true
;
let elemBelow
=
document.
elementFromPoint(
event.
clientX
,
event.
clientY)
;
ball.
hidden
=
false
;
// mousemove events may trigger out of the window (when the ball is dragged off-screen)
// if clientX/clientY are out of the window, then elementFromPoint returns null
if (
!elemBelow)
return
;
// potential droppables are labeled with the class "droppable" (can be other logic)
let droppableBelow
=
elemBelow.
closest(
'.droppable')
;
if (currentDroppable
!= droppableBelow)
{
// we're flying in or out...
// note: both values can be null
// currentDroppable=null if we were not over a droppable before this event (e.g over an empty space)
// droppableBelow=null if we're not over a droppable now, during this event
if (currentDroppable)
{
// the logic to process "flying out" of the droppable (remove highlight)
leaveDroppable(currentDroppable)
;
}
currentDroppable
= droppableBelow
;
if (currentDroppable)
{
// the logic to process "flying in" of the droppable
enterDroppable(currentDroppable)
;
}
}
}
In the example below when the ball is dragged over the soccer goal, the goal is highlighted.
[codetabs height=250 src="ball4"]
Now we have the current "drop target", that we're flying over, in the variable
currentDroppable during the whole process and can use it to highlight or any other stuff.
We considered a basic Drag'n'Drop algorithm.
The key components:
ball.mousedown ->
document.mousemove ->
ball.mouseup (don't forget to cancel native
ondragstart).
shiftX/shiftY and keep it during the dragging.
document.elementFromPoint.
We can lay a lot on this foundation.
mouseup we can intellectually finalize the drop: change data, move elements around.
mousedown/up. A large-area event handler that checks
event.target can manage Drag'n'Drop for hundreds of elements.
There are frameworks that build architecture over it:
DragZone,
Droppable,
Draggable and other classes. Most of them do the similar stuff to what's described above, so it
should be easy to understand them now. Or roll your own, as you can see that that's easy enough to do, sometimes
easier than adapting a third-party solution.
Pointer events are a modern way to handle input from a variety of pointing devices, such as a mouse, a pen/stylus, a touchscreen, and so on.
Let's make a small overview, so that you understand the general picture and the place of Pointer Events among other event types.
Long ago, in the past, there were only mouse events.
Then touch devices became widespread, phones and tablets in particular. For the existing scripts to work,
they generated (and still generate) mouse events. For instance, tapping a touchscreen generates
mousedown. So touch devices worked well with web pages.
But touch devices have more capabilities than a mouse. For example, it's possible to touch multiple points at once ("multi-touch"). Although, mouse events don't have necessary properties to handle such multi-touches.
So touch events were introduced, such as
touchstart,
touchend,
touchmove, that have touch-specific properties (we don't cover them in detail here, because
pointer events are even better).
Still, it wasn't enough, as there are many other devices, such as pens, that have their own features. Also, writing code that listens for both touch and mouse events was cumbersome.
To solve these issues, the new standard Pointer Events was introduced. It provides a single set of events for all kinds of pointing devices.
As of now, Pointer Events Level 2 specification is supported in all major browsers, while the newer Pointer Events Level 3 is in the works and is mostly compartible with Pointer Events level 2.
Unless you develop for old browsers, such as Internet Explorer 10, or for Safari 12 or below, there's no point in using mouse or touch events any more - we can switch to pointer events.
Then your code will work well with both touch and mouse devices.
That said, there are some important peculiarities that one should know in order to use Pointer Events correctly and avoid surprises. We'll make note of them in this article.
Pointer events are named similarly to mouse events:
| Pointer event | Similar mouse event |
|---|---|
pointerdown
|
mousedown
|
pointerup
|
mouseup
|
pointermove
|
mousemove
|
pointerover
|
mouseover
|
pointerout
|
mouseout
|
pointerenter
|
mouseenter
|
pointerleave
|
mouseleave
|
pointercancel
|
- |
gotpointercapture
|
- |
lostpointercapture
|
- |
As we can see, for every
mouse<event>, there's a
pointer<event> that plays a similar role. Also there are 3 additional pointer events that don't
have a corresponding
mouse... counterpart, we'll explain them soon.
``
smart header="Replacingmouse
withpointer
in our code" We can replacemouse
events withpointer
The support for touch devices will also "magically" improve. Although, we may need to add
touch-action: none in some places in CSS. We'll cover it below in the section about
pointercancel.
Pointer events have the same properties as mouse events, such as
clientX/Y,
target, etc., plus some others:
pointerId - the unique identifier of the pointer causing the event.
pointerType - the pointing device type. Must be a string, one of: "mouse", "pen" or "touch".
isPrimary - is
true for the primary pointer (the first finger in multi-touch).
Some pointer devices measure contact area and pressure, e.g. for a finger on the touchscreen, there are additional properties for that:
width - the width of the area where the pointer (e.g. a finger) touches the device. Where
unsupported, e.g. for a mouse, it's always
1.
height - the height of the area where the pointer touches the device. Where unsupported, it's
always
1.
pressure - the pressure of the pointer tip, in range from 0 to 1. For devices that don't support
pressure must be either
0.5 (pressed) or
0.
tangentialPressure - the normalized tangential pressure.
tiltX,
tiltY,
twist - pen-specific properties that describe how the pen is positioned relative the surface.
These properties aren't supported by most devices, so they are rarely used. You can find the details about them in the specification if needed.
One of the things that mouse events totally don't support is multi-touch: a user can touch in several places at once on their phone or tablet, or perform special gestures.
Pointer Events allow handling multi-touch with the help of the
pointerId and
isPrimary properties.
Here's what happens when a user touches a touchscreen in one place, then puts another finger somewhere else on it:
pointerdown with
isPrimary=true and some
pointerId.
pointerdown with
isPrimary=false and a different
pointerId for every finger.
Please note: the
pointerId is assigned not to the whole device, but for each touching finger. If we use 5 fingers to
simultaneously touch the screen, we have 5
pointerdown events, each with their respective coordinates and a different
pointerId.
The events associated with the first finger always have
isPrimary=true.
We can track multiple touching fingers using their
pointerId. When the user moves and then removes a finger, we get
pointermove and
pointerup events with the same
pointerId as we had in
pointerdown.
Here's the demo that logs `pointerdown` and `pointerup` events:
[iframe src="multitouch" edit height=200]
Please note: you must be using a touchscreen device, such as a phone or a tablet, to actually see the difference in `pointerId/isPrimary`. For single-touch devices, such as a mouse, there'll be always same `pointerId` with `isPrimary=true`, for all pointer events.
The
pointercancel event fires when there's an ongoing pointer interaction, and then something happens
that causes it to be aborted, so that no more pointer events are generated.
Such causes are: - The pointer device hardware was physically disabled. - The device orientation changed (tablet rotated). - The browser decided to handle the interaction on its own, considering it a mouse gesture or zoom-and-pan action or something else.
We'll demonstrate
pointercancel on a practical example to see how it affects us.
Let's say we're impelementing drag'n'drop for a ball, just as in the beginning of the article info:mouse-drag-and-drop.
Here is the flow of user actions and the corresponding events:
pointerdown event fires
pointermove fires, maybe several times
pointercancel event.
pointermove events for us.
So the issue is that the browser "hijacks" the interaction:
pointercancel fires in the beginning of the "drag-and-drop" process, and no more
pointermove events are generated.
Here's the drag'n'drop demo with loggin of pointer events (only `up/down`, `move` and `cancel`) in the `textarea`:
[iframe src="ball" height=240 edit]
We'd like to implement the drag'n'drop on our own, so let's tell the browser not to take it over.
Prevent the default browser action to avoid
pointercancel.
We need to do two things:
ball.ondragstart = () => false, just as described in the article
info:mouse-drag-and-drop.
#ball { touch-action: none } in CSS.
After we do that, the events will work as intended, the browser won't hijack the process and doesn't emit
pointercancel.
This demo adds these lines:
[iframe src="ball-2" height=240 edit]
As you can see, there's no `pointercancel` any more.
Now we can add the code to actually move the ball, and our drag'n'drop will work for mouse devices and touch devices.
Pointer capturing is a special feature of pointer events.
The idea is very simple, but may seem quite odd at first, as nothing like that exists for any other event type.
The main method is: -
elem.setPointerCapture(pointerId) - binds events with the given
pointerId to
elem. After the call all pointer events with the same
pointerId will have
elem as the target (as if happened on
elem), no matter where in document they really happened.
In other words,
elem.setPointerCapture(pointerId) retargets all subsequent events with the given
pointerId to
elem.
The binding is removed: - automatically when
pointerup or
pointercancel events occur, - automatically when
elem is removed from the document, - when
elem.releasePointerCapture(pointerId) is called.
Pointer capturing can be used to simplify drag'n'drop kind of interactions.
As an example, let's recall how one can implement a custom slider, described in the info:mouse-drag-and-drop.
We make a slider element with the strip and the "runner" (
thumb) inside it.
Then it works like this:
thumb -
pointerdown triggers.
pointermove triggers, and we move the
thumb along.
thumb: go above or below it. The
thumb should move strictly horizontally, remaining aligned with the pointer.
So, to track all pointer movements, including when it goes above/below the
thumb, we had to assign
pointermove event handler on the whole
document.
That solution looks a bit "dirty". One of the problems is that pointer movements around the document may cause side effects, trigger other event handlers, totally not related to the slider.
Pointer capturing provides a means to bind
pointermove to
thumb and avoid any such problems:
thumb.setPointerCapture(event.pointerId) in
pointerdown handler,
pointerup/cancel will be retargeted to
thumb.
pointerup happens (dragging complete), the binding is removed automatically, we don't need to care
about it.
So, even if the user moves the pointer around the whole document, events handlers will be called on
thumb. Besides, coordinate properties of the event objects, such as
clientX/clientY will still be correct - the capturing only affects
target/currentTarget.
Here's the essential code:
thumb.
onpointerdown
=
function(event)
{
// retarget all pointer events (until pointerup) to thumb
thumb.
setPointerCapture(
event.
pointerId)
;
};
thumb.
onpointermove
=
function(event)
{
// moving the slider: listen on the thumb, as all pointer events are retargeted to it
let newLeft
=
event.
clientX
-
slider.
getBoundingClientRect().
left
;
thumb.
style.
left
= newLeft
+
'px'
;
};
// note: no need to call thumb.releasePointerCapture,
// it happens on pointerup automatically
The full demo:
[iframe src="slider" height=100 edit]
At the end, pointer capturing gives us two benefits: 1. The code becomes cleaner as we don't need to add/remove
handlers on the whole
document any more. The binding is released automatically. 2. If there are any
pointermove handlers in the document, they won't be accidentally triggered by the pointer while the
user is dragging the slider.
There are two associated pointer events:
gotpointercapture fires when an element uses
setPointerCapture to enable capturing.
lostpointercapture fires when the capture is released: either explicitly with
releasePointerCapture call, or automatically on
pointerup/
pointercancel.
Pointer events allow handling mouse, touch and pen events simultaneously, with a single piece of code.
Pointer events extend mouse events. We can replace
mouse with
pointer in event names and expect our code to continue working for mouse, with better support for
other device types.
For drag'n'drops and complex touch interactions that the browser may decide to hijack and handle on its own -
remember to cancel the default action on events and set
touch-events: none in CSS for elements that we engage.
Additional abilities of pointer events are:
pointerId and
isPrimary.
pressure,
width/height, and others.
pointerup/
pointercancel.
As of now, pointer events are supported in all major browsers, so we can safely switch to them, especially if IE10- and Safari 12- are not needed. And even with those browsers, there are polyfills that enable the support of pointer events.
Before we get to keyboard, please note that on modern devices there are other ways to "input something". For instance, people use speech recognition (especially on mobile devices) or copy/paste with the mouse.
So if we want to track any input into an
<input> field, then keyboard events are not enough. There's another event named
input to track changes of an
<input> field, by any means. And it may be a better choice for such task. We'll cover it later
in the chapter
info:events-change-input.
Keyboard events should be used when we want to handle keyboard actions (virtual keyboard also counts). For
instance, to react on arrow keys
key:Up and
key:Down or hotkeys (including combinations of keys).
To better understand keyboard events, you can use the [teststand](sandbox:keyboard-dump).
To better understand keyboard events, you can use the teststand below.
Try different key combinations in the text field.
[codetabs src="keyboard-dump" height=480]
The
keydown events happens when a key is pressed down, and then
keyup - when it's released.
The
key property of the event object allows to get the character, while the
code property of the event object allows to get the "physical key code".
For instance, the same key
key:Z can be pressed with or without
key:Shift. That gives us two different characters: lowercase
z and uppercase
Z.
The
event.key is exactly the character, and it will be different. But
event.code is the same:
| Key |
event.key
|
event.code
|
|---|---|---|
key:Z
|
z (lowercase)
|
KeyZ
|
key:Shift+Z
|
Z (uppercase)
|
KeyZ
|
If a user works with different languages, then switching to another language would make a totally different
character instead of
"Z". That will become the value of
event.key, while
event.code is always the same:
"KeyZ".
smart header=""KeyZ" and other key codes" Every key has the code that depends on its location on the keyboard. Key codes described in the UI Events code specification.
For instance: - Letter keys have codes
"Key<letter>":
"KeyA",
"KeyB" etc. - Digit keys have codes:
"Digit<number>":
"Digit0",
"Digit1" etc. - Special keys are coded by their names:
"Enter",
"Backspace",
"Tab" etc.
There are several widespread keyboard layouts, and the specification gives key codes for each of them.
Read the alphanumeric section of the spec for more codes, or just press a key in the teststand above.
``
warn header="Case matters:"KeyZ"
, not"keyZ"`" Seems obvious, but people still make mistakes.
Please evade mistypes: it's
KeyZ, not
keyZ. The check like
event.code=="keyZ" won't work: the first letter of
"Key" must be uppercase.
What if a key does not give any character? For instance,
key:Shift or
key:F1 or others. For those keys,
event.key is approximately the same as
event.code:
| Key |
event.key
|
event.code
|
|---|---|---|
key:F1
|
F1
|
F1
|
key:Backspace
|
Backspace
|
Backspace
|
key:Shift
|
Shift
|
ShiftRight or
ShiftLeft
|
Please note that
event.code specifies exactly which key is pressed. For instance, most keyboards have two
key:Shift keys: on the left and on the right side. The
event.code tells us exactly which one was pressed, and
event.key is responsible for the "meaning" of the key: what it is (a "Shift").
Let's say, we want to handle a hotkey:
key:Ctrl+Z (or
key:Cmd+Z for Mac). Most text editors hook the "Undo" action on it. We can set a listener on
keydown and check which key is pressed.
There's a dilemma here: in such a listener, should we check the value of
event.key or
event.code?
On one hand, the value of
event.key is a character, it changes depending on the language. If the visitor has several languages
in OS and switches between them, the same key gives different characters. So it makes sense to check
event.code, it's always the same.
Like this:
js run document.addEventListener('keydown', function(event) { if (event.code == 'KeyZ' && (event.ctrlKey || event.metaKey)) { alert('Undo!') } });
On the other hand, there's a problem with
event.code. For different keyboard layouts, the same key may have different characters.
For example, here are US layout ("QWERTY") and German layout ("QWERTZ") under it (from Wikipedia):
For the same key, US layout has "Z", while German layout has "Y" (letters are swapped).
Literally,
event.code will equal
KeyZ for people with German layout when they press
key:Y.
If we check
event.code == 'KeyZ' in our code, then for people with German layout such test will pass when they
press
key:Y.
That sounds really odd, but so it is. The specification explicitly mentions such behavior.
So,
event.code may match a wrong character for unexpected layout. Same letters in different layouts may
map to different physical keys, leading to different codes. Luckily, that happens only with several codes, e.g.
keyA,
keyQ,
keyZ (as we've seen), and doesn't happen with special keys such as
Shift. You can find the list in the
specification.
To reliably track layout-dependent characters,
event.key may be a better way.
On the other hand,
event.code has the benefit of staying always the same, bound to the physical key location, even if
the visitor changes languages. So hotkeys that rely on it work well even in case of a language switch.
Do we want to handle layout-dependant keys? Then
event.key is the way to go.
Or we want a hotkey to work even after a language switch? Then
event.code may be better.
If a key is being pressed for a long enough time, it starts to "auto-repeat": the
keydown triggers again and again, and then when it's released we finally get
keyup. So it's kind of normal to have many
keydown and a single
keyup.
For events triggered by auto-repeat, the event object has
event.repeat property set to
true.
Default actions vary, as there are many possible things that may be initiated by the keyboard.
For instance:
key:Delete key).
key:PageDown key).
key:Ctrl+S)
Preventing the default action on
keydown can cancel most of them, with the exception of OS-based special keys. For instance, on
Windows
key:Alt+F4 closes the current browser window. And there's no way to stop it by preventing the default
action in JavaScript.
For instance, the
<input> below expects a phone number, so it does not accept keys except digits,
+,
() or
-:
html autorun height=60 run <script> function checkPhoneKey(key) { return (key >= '0' && key <= '9') || key == '+' || key == '(' || key == ')' || key == '-'; } </script> <input *!*onkeydown="return checkPhoneKey(event.key)"*/!* placeholder="Phone, please" type="tel">
Please note that special keys, such as
key:Backspace,
key:Left,
key:Right,
key:Ctrl+V, do not work in the input. That's a side-effect of the strict filter
checkPhoneKey.
Let's relax it a little bit:
html autorun height=60 run <script> function checkPhoneKey(key) { return (key >= '0' && key <= '9') || key == '+' || key == '(' || key == ')' || key == '-' || key == 'ArrowLeft' || key == 'ArrowRight' || key == 'Delete' || key == 'Backspace'; } </script> <input onkeydown="return checkPhoneKey(event.key)" placeholder="Phone, please" type="tel">
Now arrows and deletion works well.
…But we still can enter anything by using a mouse and right-click + Paste. So the filter is not 100% reliable. We
can just let it be like that, because most of time it works. Or an alternative approach would be to track the
input event - it triggers after any modification. There we can check the new value and
highlight/modify it when it's invalid.
In the past, there was a
keypress event, and also
keyCode,
charCode,
which properties of the event object.
There were so many browser incompatibilities while working with them, that developers of the specification had no way, other than deprecating all of them and creating new, modern events (described above in this chapter). The old code still works, as browsers keep supporting them, but there's totally no need to use those any more.
When using virtual/mobile keyboards, formally known as IME (Input-Method Editor), the W3C standard states that a
KeyboardEvent's
e.keyCode should be
229
and
e.key should be
"Unidentified"
.
While some of these keyboards might still use the right values for
e.key,
e.code,
e.keyCode… when pressing certain keys such as arrows or backspace, there's no guarantee, so your
keyboard logic might not always work on mobile devices.
Pressing a key always generates a keyboard event, be it symbol keys or special keys like
key:Shift or
key:Ctrl and so on. The only exception is
key:Fn key that sometimes presents on a laptop keyboard. There's no keyboard event for it, because
it's often implemented on lower level than OS.
Keyboard events:
keydown - on pressing the key (auto-repeats if the key is pressed for long),
keyup - on releasing the key.
Main keyboard event properties:
code - the "key code" (
"KeyA",
"ArrowLeft" and so on), specific to the physical location of the key on keyboard.
key - the character (
"A",
"a" and so on), for non-character keys, such as
key:Esc, usually has the same value as
code.
In the past, keyboard events were sometimes used to track user input in form fields. That's not reliable, because
the input can come from various sources. We have
input and
change events to handle any input (covered later in the chapter
info:events-change-input). They trigger after any kind of
input, including copy-pasting or speech recognition.
We should use keyboard events when we really want keyboard. For example, to react on hotkeys or special keys.
The
scroll event allows reacting to a page or element scrolling. There are quite a few good things we can
do here.
For instance: - Show/hide additional controls or information depending on where in the document the user is. - Load more data when the user scrolls down till the end of the page.
Here's a small function to show the current scroll:
js autorun window.addEventListener('scroll', function() { document.getElementById('showScroll').innerHTML = window.pageYOffset + 'px'; });
In action:
Current scroll = <b id="showScroll">scroll the window</b>
The
scroll event works both on the
window and on scrollable elements.
How do we make something unscrollable?
We can't prevent scrolling by using
event.preventDefault() in
onscroll listener, because it triggers
after the scroll has already happened.
But we can prevent scrolling by
event.preventDefault() on an event that causes the scroll, for instance
keydown event for
key:pageUp and
key:pageDown.
If we add an event handler to these events and
event.preventDefault() in it, then the scroll won't start.
There are many ways to initiate a scroll, so it's more reliable to use CSS,
overflow property.
Here are few tasks that you can solve or look through to see applications of
onscroll.
Forms and control elements, such as
<input> have a lot of special properties and events.
Working with forms will be much more convenient when we learn them.
Document forms are members of the special collection
document.forms.
That's a so-called "named collection": it's both named and ordered. We can use both the name or the number in the document to get the form.
js no-beautify document.forms.my; // the form with name="my" document.forms[0]; // the first form in the document
When we have a form, then any element is available in the named collection
form.elements.
For instance:
run height=40
There may be multiple elements with the same name. This is typical with radio buttons and checkboxes.
In that case,
form.elements[name] is a
collection. For instance:
These navigation properties do not depend on the tag structure. All control elements, no matter how deep they are
in the form, are available in
form.elements.
smart header="Fieldsets as \"subforms\"" A form may have one or many
`;
} });
/!
All the unslotted light DOM content gets into the "Other information" fieldset.
Elements are appended to a slot one after another, so both unslotted pieces of information are in the default slot together.
The flattened DOM looks like this:
<user-card>
#shadow-root
<div>Name:
<slot
name=
"username"
>
<span
slot=
"username"
>John Smith
</span>
</slot>
</div>
<div>Birthday:
<slot
name=
"birthday"
>
<span
slot=
"birthday"
>01.01.2001
</span>
</slot>
</div>
<fieldset>
<legend>Other information
</legend>
*!*
<slot>
<div>I like to swim.
</div>
<div>...And play volleyball too!
</div>
</slot>
*/!*
</fieldset>
</user-card>
Now let's back to
<custom-menu>, mentioned at the beginning of the chapter.
We can use slots to distribute elements.
Here's the markup for
<custom-menu>:
<custom-menu>
<span
slot=
"title"
>Candy menu
</span>
<li
slot=
"item"
>Lollipop
</li>
<li
slot=
"item"
>Fruit Toast
</li>
<li
slot=
"item"
>Cup Cake
</li>
</custom-menu>
The shadow DOM template with proper slots:
<template
id=
"tmpl"
>
<style>
/* menu styles */
</style>
<div
class=
"menu"
>
<slot
name=
"title"
></slot>
<ul><slot
name=
"item"
></slot></ul>
</div>
</template>
<span slot="title"> goes into
<slot name="title">.
<li slot="item"> in the template, but only one
<slot name="item"> in the template. So all such
<li slot="item"> are appended to
<slot name="item"> one after another, thus forming the list.
The flattened DOM becomes:
<custom-menu>
#shadow-root
<style>
/* menu styles */
</style>
<div
class=
"menu"
>
<slot
name=
"title"
>
<span
slot=
"title"
>Candy menu
</span>
</slot>
<ul>
<slot
name=
"item"
>
<li
slot=
"item"
>Lollipop
</li>
<li
slot=
"item"
>Fruit Toast
</li>
<li
slot=
"item"
>Cup Cake
</li>
</slot>
</ul>
</div>
</custom-menu>
One might notice that, in a valid DOM,
<li> must be a direct child of
<ul>. But that's flattened DOM, it describes how the component is rendered, such thing happens
naturally here.
We just need to add a
click handler to open/close the list, and the
<custom-menu> is ready:
customElements.
define(
'custom-menu'
,
class
extends HTMLElement
{
connectedCallback()
{
this.
attachShadow(
{
mode
:
'open'
})
;
// tmpl is the shadow DOM template (above)
this.
shadowRoot.
append(
tmpl.
content.
cloneNode(
true) )
;
// we can't select light DOM nodes, so let's handle clicks on the slot
this.
shadowRoot.
querySelector(
'slot[name="title"]').
onclick
= ()
=>
{
// open/close the menu
this.
shadowRoot.
querySelector(
'.menu').
classList.
toggle(
'closed')
;
};
}
})
;
Here's the full demo:
[iframe src="menu" height=140 edit]
Of course, we can add more functionality to it: events, methods and so on.
What if the outer code wants to add/remove menu items dynamically?
The browser monitors slots and updates the rendering if slotted elements are added/removed.
Also, as light DOM nodes are not copied, but just rendered in slots, the changes inside them immediately become visible.
So we don't have to do anything to update rendering. But if the component code wants to know about slot changes,
then
slotchange event is available.
For example, here the menu item is inserted dynamically after 1 second, and the title changes after 2 seconds:
run untrusted height=80
The menu rendering updates each time without our intervention.
There are two
slotchange events here:
At initialization:
slotchange: title triggers immediately, as the
slot="title" from the light DOM gets into the corresponding slot.
After 1 second:
slotchange: item triggers, when a new
<li slot="item"> is added.
Please note: there's no
slotchange event after 2 seconds, when the content of
slot="title" is modified. That's because there's no slot change. We modify the content inside the
slotted element, that's another thing.
If we'd like to track internal modifications of light DOM from JavaScript, that's also possible using a more generic mechanism: MutationObserver.
Finally, let's mention the slot-related JavaScript methods.
As we've seen before, JavaScript looks at the "real" DOM, without flattening. But, if the shadow tree has
{mode: 'open'}, then we can figure out which elements assigned to a slot and, vise-versa, the slot by
the element inside it:
node.assignedSlot - returns the
<slot> element that the
node is assigned to.
slot.assignedNodes({flatten: true/false}) - DOM nodes, assigned to the slot. The
flatten option is
false by default. If explicitly set to
true, then it looks more deeply into the flattened DOM, returning nested slots in case of nested
components and the fallback content if no node assigned.
slot.assignedElements({flatten: true/false}) - DOM elements, assigned to the slot (same as above,
but only element nodes).
These methods are useful when we need not just show the slotted content, but also track it in JavaScript.
For example, if
<custom-menu> component wants to know, what it shows, then it could track
slotchange and get the items from
slot.assignedElements:
Usually, if an element has shadow DOM, then its light DOM is not displayed. Slots allow to show elements from light DOM in specified places of shadow DOM.
There are two kinds of slots:
<slot name="X">...</slot> - gets light children with
slot="X".
<slot> without a name (subsequent unnamed slots are ignored) - gets unslotted light children.
<slot> element is used as a fallback. It's shown if there are no light children for the slot.
The process of rendering slotted elements inside their slots is called "composition". The result is called a "flattened DOM".
Composition does not really move nodes, from JavaScript point of view the DOM is still same.
JavaScript can access slots using methods: -
slot.assignedNodes/Elements() - returns nodes/elements inside the
slot. -
node.assignedSlot - the reverse property, returns slot by a node.
If we'd like to know what we're showing, we can track slot contents using: -
slotchange event - triggers the first time a slot is filled, and on any add/remove/replace operation
of the slotted element, but not its children. The slot is
event.target. -
MutationObserver to go deeper into slot content, watch changes inside it.
Now, as we know how to show elements from light DOM in shadow DOM, let's see how to style them properly. The basic rule is that shadow elements are styled inside, and light elements - outside, but there are notable exceptions.
We'll see the details in the next chapter.
Shadow DOM may include both
<style> and
<link rel="stylesheet" href="…"> tags. In the latter case, stylesheets are HTTP-cached, so they
are not redownloaded for multiple components that use same template.
As a general rule, local styles work only inside the shadow tree, and document styles work outside of it. But there are few exceptions.
The
:host selector allows to select the shadow host (the element containing the shadow tree).
For instance, we're making
<custom-dialog> element that should be centered. For that we need to style the
<custom-dialog> element itself.
That's exactly what
:host does:
The shadow host (
<custom-dialog> itself) resides in the light DOM, so it's affected by document CSS rules.
If there's a property styled both in
:host locally, and in the document, then the document style takes precedence.
For instance, if in the document we had:
…Then the
<custom-dialog> would be without padding.
It's very convenient, as we can setup "default" component styles in its
:host rule, and then easily override them in the document.
The exception is when a local property is labelled
!important, for such properties, local styles take precedence.
Same as
:host, but applied only if the shadow host matches the
selector.
For example, we'd like to center the
<custom-dialog> only if it has
centered attribute:
Now the additional centering styles are only applied to the first dialog:
<custom-dialog centered>.
Same as
:host, but applied only if the shadow host or any of its ancestors in the outer document matches the
selector.
E.g.
:host-context(.dark-theme) matches only if there's
dark-theme class on
<custom-dialog> on anywhere above it:
<body
class=
"dark-theme"
>
<!--
:host-context(.dark-theme) applies to custom-dialogs inside .dark-theme
-->
<custom-dialog>...
</custom-dialog>
</body>
To summarize, we can use
:host-family of selectors to style the main element of the component, depending on the context. These
styles (unless
!important) can be overridden by the document.
Now let's consider the situation with slots.
Slotted elements come from light DOM, so they use document styles. Local styles do not affect slotted content.
In the example below, slotted<span> is bold, as per document style, but does not take
background from the local style: run autorun="no-epub" untrusted height=80
The result is bold, but not red.
If we'd like to style slotted elements in our component, there are two choices.
First, we can style the
<slot> itself and rely on CSS inheritance:
Here
<p>John Smith</p> becomes bold, because CSS inheritance is in effect between the
<slot> and its contents. But in CSS itself not all properties are inherited.
Another option is to use
::slotted(selector) pseudo-class. It matches elements based on two conditions:
selector.
In our example,
::slotted(div) selects exactly
<div slot="username">, but not its children:
<div>John Smith</div>
Please note,
::slotted selector can't descend any further into the slot. These selectors are invalid:
::slotted(div span) {
/* our slotted <div> does not match this */
}
::slotted(div) p {
/* can't go inside light DOM */
}
Also,
::slotted can only be used in CSS. We can't use it in
querySelector.
How do we style internal elements of a component from the main document?
Selectors like
:host apply rules to
<custom-dialog> element or
<user-card>, but how to style shadow DOM elements inside them?
There's no selector that can directly affect shadow DOM styles from the document. But just as we expose methods to interact with our component, we can expose CSS variables (custom CSS properties) to style it.
Custom CSS properties exist on all levels, both in light and shadow.
For example, in shadow DOM we can use
--user-card-field-color CSS variable to style fields, and the outer document can set its value:
<style>
.field {
color:
var(
--user-card-field-color
,
black
)
;
/* if --user-card-field-color is not defined, use black color */
}
</style>
<div
class=
"field"
>Name:
<slot
name=
"username"
></slot></div>
<div
class=
"field"
>Birthday:
<slot
name=
"birthday"
></slot></div>
Then, we can declare this property in the outer document for
<user-card>:
Custom CSS properties pierce through shadow DOM, they are visible everywhere, so the inner
.field rule will make use of it.
Here's the full example:
run autorun="no-epub" untrusted height=80
Shadow DOM can include styles, such as
<style> or
<link rel="stylesheet">.
Local styles can affect: - shadow tree, - shadow host with
:host-family pseudoclasses, - slotted elements (coming from light DOM),
::slotted(selector) allows to select slotted elements themselves, but not their children.
Document styles can affect: - shadow host (as it lives in the outer document) - slotted elements and their contents (as that's also in the outer document)
When CSS properties conflict, normally document styles have precedence, unless the property is labelled as
!important. Then local styles have precedence.
CSS custom properties pierce through shadow DOM. They are used as "hooks" to style the component:
var(--component-name-title, <default value>).
--component-name-title CSS property for the shadow host or above.
The idea behind shadow tree is to encapsulate internal implementation details of a component.
Let's say, a click event happens inside a shadow DOM of
<user-card> component. But scripts in the main document have no idea about the shadow DOM
internals, especially if the component comes from a 3rd-party library.
So, to keep the details encapsulated, the browser retargets the event.
Events that happen in shadow DOM have the host element as the target, when caught outside of the component.
Here's a simple example:
run autorun="no-epub" untrusted height=60
If you click on the button, the messages are:
BUTTON - internal event handler gets the correct target, the element inside shadow DOM.
USER-CARD - document event handler gets shadow host as the target.
Event retargeting is a great thing to have, because the outer document doesn't have to know about component
internals. From its point of view, the event happened on
<user-card>.
Retargeting does not occur if the event occurs on a slotted element, that physically lives in the light DOM.
For example, if a user clicks on
<span slot="username"> in the example below, the event target is exactly this
span element, for both shadow and light handlers:
run autorun="no-epub" untrusted height=60
If a click happens on
"John Smith", for both inner and outer handlers the target is
<span slot="username">. That's an element from the light DOM, so no retargeting.
On the other hand, if the click occurs on an element originating from shadow DOM, e.g. on
<b>Name</b>, then, as it bubbles out of the shadow DOM, its
event.target is reset to
<user-card>.
For purposes of event bubbling, flattened DOM is used.
So, if we have a slotted element, and an event occurs somewhere inside it, then it bubbles up to the
<slot> and upwards.
The full path to the original event target, with all the shadow elements, can be obtained using
event.composedPath(). As we can see from the name of the method, that path is taken after the
composition.
In the example above, the flattened DOM is:
<user-card
id=
"userCard"
>
#shadow-root
<div>
<b>Name:
</b>
<slot
name=
"username"
>
<span
slot=
"username"
>John Smith
</span>
</slot>
</div>
</user-card>
So, for a click on
<span slot="username">, a call to
event.composedPath() returns an array: [
span,
slot,
div,
shadow-root,
user-card,
body,
html,
document,
window]. That's exactly the parent chain from the target element in the flattened DOM, after the
composition.
``
warn header="Shadow tree details are only provided for{mode:‘open'}
trees" If the shadow tree was created with{mode: ‘closed'}
, then the composed path starts from the host:user-card` and upwards.
That's the similar principle as for other methods that work with shadow DOM. Internals of closed trees are completely hidden.
Most events successfully bubble through a shadow DOM boundary. There are few events that do not.
This is governed by the
composed event object property. If it's
true, then the event does cross the boundary. Otherwise, it only can be caught from inside the shadow
DOM.
If you take a look at
UI Events specification, most events have
composed: true:
blur,
focus,
focusin,
focusout,
click,
dblclick,
mousedown,
mouseup
mousemove,
mouseout,
mouseover,
wheel,
beforeinput,
input,
keydown,
keyup.
All touch events and pointer events also have
composed: true.
There are some events that have
composed: false though:
mouseenter,
mouseleave (they do not bubble at all),
load,
unload,
abort,
error,
select,
slotchange.
These events can be caught only on elements within the same DOM, where the event target resides.
When we dispatch custom events, we need to set both
bubbles and
composed properties to
true for it to bubble up and out of the component.
For example, here we create
div#inner in the shadow DOM of
div#outer and trigger two events on it. Only the one with
composed: true makes it outside to the document:
Events only cross shadow DOM boundaries if their
composed flag is set to
true.
Built-in events mostly have
composed: true, as described in the relevant specifications:
Some built-in events that have
composed: false:
mouseenter,
mouseleave (also do not bubble),
load,
unload,
abort,
error,
select,
slotchange.
These events can be caught only on elements within the same DOM.
If we dispatch a
CustomEvent, then we should explicitly set
composed: true.
Please note that in case of nested components, one shadow DOM may be nested into another. In that case composed
events bubble through all shadow DOM boundaries. So, if an event is intended only for the immediate enclosing
component, we can also dispatch it on the shadow host and set
composed: false. Then it's out of the component shadow DOM, but won't bubble up to higher-level DOM.
Regular expressions are patterns that provide a powerful way to search and replace in text.
In JavaScript, they are available via the RegExp object, as well as being integrated in methods of strings.
A regular expression (also "regexp", or just "reg") consists of a pattern and optional flags.
There are two syntaxes that can be used to create a regular expression object.
The "long" syntax:
And the "short" one, using slashes
"/":
regexp
=
/pattern/
;
// no flags
regexp
=
/pattern/gmi
;
// with flags g,m and i (to be covered soon)
Slashes
pattern:/.../ tell JavaScript that we are creating a regular expression. They play the same role as
quotes for strings.
In both cases
regexp becomes an instance of the built-in
RegExp class.
The main difference between these two syntaxes is that pattern using slashes
/.../ does not allow for expressions to be inserted (like string template literals with
${...}). They are fully static.
Slashes are used when we know the regular expression at the code writing time - and that's the most common
situation. While
new RegExp is more often used when we need to create a regexp "on the fly" from a dynamically
generated string. For instance:
let tag
=
prompt(
"What tag do you want to find?"
,
"h2")
;
let regexp
=
new
RegExp(
`<
${tag
}
>`)
;
// same as /<h2>/ if answered "h2" in the prompt above
Regular expressions may have flags that affect the search.
There are only 6 of them in JavaScript:
pattern:i
A and
a (see the example below).
pattern:g
pattern:m
pattern:s
pattern:. to match newline character
\n (covered in the chapter
info:regexp-character-classes).
pattern:u
pattern:y
smart header="Colors" From here on the color scheme is:
pattern:red
subject:blue
match:green
As mentioned previously, regular expressions are integrated with string methods.
The method
str.match(regexp) finds all matches of
regexp in the string
str.
It has 3 working modes:
If the regular expression has flag
pattern:g, it returns an array of all matches: run let str = "We will, we will rock you";
alert( str.match(/we/gi) ); // We,we (an array of 2 substrings that match) ``
Please note that bothmatch:We
andmatch:we
are found, because flagpattern:i` makes the regular expression case-insensitive.
If there's no such flag it returns only the first match in the form of an array, with the full match at index
0 and some additional details in properties: run let str = "We will, we will rock you";
let result = str.match(/we/i); // without flag g
alert( result[0] ); // We (1st match) alert( result.length ); // 1
// Details: alert( result.index ); // 0 (position of the match) alert( result.input ); // We will, we will
rock you (source string) ``
The array may have other indexes, besides0` if a part of the regular expression is enclosed in
parentheses. We'll cover that in the chapter
info:regexp-groups.
And, finally, if there are no matches,
null is returned (doesn't matter if there's flag
pattern:g or not).
This a very important nuance. If there are no matches, we don't receive an empty array, but instead receive
null. Forgetting about that may lead to errors, e.g.:
run let matches = "JavaScript".match(/HTML/); // = null
if (!matches.length) { // Error: Cannot read property ‘length' of null alert("Error in the line above"); }
If we'd like the result to always be an array, we can write it this way:
run let matches = "JavaScript".match(/HTML/) ! || [] /!;
if (!matches.length) { alert("No matches"); // now it works }
The method
str.replace(regexp, replacement) replaces matches found using
regexp in string
str with
replacement (all matches if there's flag
pattern:g, otherwise, only the first one).
For instance:
run // no flag g alert( "We will, we will".replace(/we/i, "I") ); // I will, we will
// with flag g alert( "We will, we will".replace(/we/ig, "I") ); // I will, I will
The second argument is the
replacement string. We can use special character combinations in it to insert fragments of the match:
| Symbols | Action in the replacement string |
|---|---|
$&
|
inserts the whole match |
$`</code>|inserts a part of the string before the match| |`$'''
|
An example with
pattern:$&:
js run alert( "I love HTML".replace(/HTML/, "$& and JavaScript") ); // I love HTML and JavaScript
The method
regexp.test(str) looks for at least one match, if found, returns
true, otherwise
false.
run let str = "I love JavaScript"; let regexp = /LOVE/i;
alert( regexp.test(str) ); // true
Later in this chapter we'll study more regular expressions, walk through more examples, and also meet other methods.
Full information about the methods is given in the article info:regexp-methods.
pattern:g,
pattern:i,
pattern:m,
pattern:u,
pattern:s,
pattern:y.
str.match(regexp) looks for matches: all of them if there's
pattern:g flag, otherwise, only the first one.
str.replace(regexp, replacement) replaces matches found using
regexp with
replacement: all of them if there's
pattern:g flag, otherwise only the first one.
regexp.test(str) returns
true if there's at least one match, otherwise, it returns
false.
Consider a practical task - we have a phone number like
"+7(903)-123-45-67", and we need to turn it into pure numbers:
79031234567.
To do so, we can find and remove anything that's not a number. Character classes can help with that.
A character class is a special notation that matches any symbol from a certain set.
For the start, let's explore the "digit" class. It's written as
pattern:\d and corresponds to "any single digit".
For instance, let's find the first digit in the phone number:
run let str = "+7(903)-123-45-67";
let regexp = /;
alert( str.match(regexp) ); // 7
Without the flag
pattern:g, the regular expression only looks for the first match, that is the first digit
pattern:\d.
Let's add the
pattern:g flag to find all digits:
run let str = "+7(903)-123-45-67";
let regexp = /g;
alert( str.match(regexp) ); // array of matches: 7,9,0,3,1,2,3,4,5,6,7
// let's make the digits-only phone number of them: alert( str.match(regexp).join('') ); // 79031234567
That was a character class for digits. There are other character classes as well.
Most used are:
pattern:\d ("d" is from "digit")
0 to
9.
pattern:\s ("s" is from "space")
\t, newlines
\n and few other rare characters, such as
\v,
\f and
\r.
pattern:\w ("w" is from "word")
_. Non-Latin letters (like cyrillic or hindi) do not belong to
pattern:\w.
For instance,
pattern:\d\s\w means a "digit" followed by a "space character" followed by a "wordly character", such
as
match:1 a.
A regexp may contain both regular symbols and character classes.
For instance,
pattern:CSS\d matches a string
match:CSS with a digit after it:
run let str = "Is there CSS4?"; let regexp = /CSS
alert( str.match(regexp) ); // CSS4
Also we can use many character classes:
js run alert( "I love HTML5!".match(/\s\w\w\w\w\d/) ); // ' HTML5'
The match (each regexp character class has the corresponding result character):
For every character class there exists an "inverse class", denoted with the same letter, but uppercased.
The "inverse" means that it matches all other characters, for instance:
pattern:\D
pattern:\d, for instance a letter.
pattern:\S
pattern:\s, for instance a letter.
pattern:\W
pattern:\w, e.g a non-latin letter or a space.
In the beginning of the chapter we saw how to make a number-only phone number from a string like
subject:+7(903)-123-45-67: find all digits and join them.
run let str = "+7(903)-123-45-67";
alert( str.match(/g).join('') ); // 79031234567
An alternative, shorter way is to find non-digits
pattern:\D and remove them from the string:
run let str = "+7(903)-123-45-67";
alert( str.replace(//g, "") ); // 79031234567
A dot
pattern:. is a special character class that matches "any character except a newline".
For instance:
js run alert( "Z".match(/./) ); // Z
Or in the middle of a regexp:
run let regexp = /CS.4/;
alert( "CSS4".match(regexp) ); // CSS4 alert( "CS-4".match(regexp) ); // CS-4 alert( "CS 4".match(regexp) ); // CS 4 (space is also a character)
Please note that a dot means "any character", but not the "absence of a character". There must be a character to match it:
js run alert( "CS4".match(/CS.4/) ); // null, no match because there's no character for the dot
By default, a dot doesn't match the newline character
\n.
For instance, the regexp
pattern:A.B matches
match:A, and then
match:B with any character between them, except a newline
\n:
js run alert( "A\nB".match(/A.B/) ); // null (no match)
There are many situations when we'd like a dot to mean literally "any character", newline included.
That's what flag
pattern:s does. If a regexp has it, then a dot
pattern:. matches literally any character:
js run alert( "A\nB".match(/A.B/s) ); // A\nB (match!)
warn header="Not supported in IE" Thepattern:s` flag is not supported in IE.
Luckily, there's an alternative, that works everywhere. We can use a regexp like
pattern:[\s\S] to match "any character" (this pattern will be covered in the article
info:regexp-character-sets-and-ranges).
js run alert( "A\nB".match(/A[\s\S]B/) ); // A\nB (match!)
The pattern
pattern:[\s\S] literally says: "a space character OR not a space character". In other words,
"anything". We could use another pair of complementary classes, such as
pattern:[\d\D], that doesn't matter. Or even the
pattern:[^] - as it means match any character except nothing.
Also we can use this trick if we want both kind of "dots" in the same pattern: the actual dot
pattern:. behaving the regular way ("not including a newline"), and also a way to match "any
character" with
pattern:[\s\S] or alike.
warn header="Pay attention to spaces" Usually we pay little attention to spaces. For us stringssubject:1-5
andsubject:1 - 5` are nearly identical.
But if a regexp doesn't take spaces into account, it may fail to work.
Let's try to find digits separated by a hyphen:
js run alert( "1 - 5".match(/\d-\d/) ); // null, no match!
Let's fix it adding spaces into the regexp
pattern:\d - \d:
js run alert( "1 - 5".match(/\d - \d/) ); // 1 - 5, now it works // or we can use \s class: alert( "1 - 5".match(/\d\s-\s\d/) ); // 1 - 5, also works
A space is a character. Equal in importance with any other character.
We can't add or remove spaces from a regular expression and expect it to work the same.
In other words, in a regular expression all characters matter, spaces too.
There exist following character classes:
pattern:\d - digits.
pattern:\D - non-digits.
pattern:\s - space symbols, tabs, newlines.
pattern:\S - all but
pattern:\s.
pattern:\w - Latin letters, digits, underscore
'_'.
pattern:\W - all but
pattern:\w.
pattern:. - any character if with the regexp
's' flag, otherwise any except a newline
\n.
…But that's not all!
Unicode encoding, used by JavaScript for strings, provides many properties for characters, like: which language the letter belongs to (if it's a letter), is it a punctuation sign, etc.
We can search by these properties as well. That requires flag
pattern:u, covered in the next article.
JavaScript uses Unicode encoding for strings. Most characters are encoded with 2 bytes, but that allows to represent at most 65536 characters.
That range is not big enough to encode all possible characters, that's why some rare characters are encoded with
4 bytes, for instance like
𝒳 (mathematical X) or
😄 (a smile), some hieroglyphs and so on.
Here are the Unicode values of some characters:
| Character | Unicode | Bytes count in Unicode |
|---|---|---|
| a |
0x0061
|
2 |
| ≈ |
0x2248
|
2 |
| 𝒳 |
0x1d4b3
|
4 |
| 𝒴 |
0x1d4b4
|
4 |
| 😄 |
0x1f604
|
4 |
So characters like
a and
≈ occupy 2 bytes, while codes for
𝒳,
𝒴 and
😄 are longer, they have 4 bytes.
Long time ago, when JavaScript language was created, Unicode encoding was simpler: there were no 4-byte characters. So, some language features still handle them incorrectly.
For instance,
length thinks that here are two characters:
js run alert('😄'.length); // 2 alert('𝒳'.length); // 2
…But we can see that there's only one, right? The point is that
length treats 4 bytes as two 2-byte characters. That's incorrect, because they must be considered
only together (so-called "surrogate pair", you can read about them in the article
info:string).
By default, regular expressions also treat 4-byte "long characters" as a pair of 2-byte ones. And, as it happens with strings, that may lead to odd results. We'll see that a bit later, in the article info:regexp-character-sets-and-ranges.
Unlike strings, regular expressions have flag
pattern:u that fixes such problems. With such flag, a regexp handles 4-byte characters correctly. And
also Unicode property search becomes available, we'll get to it next.
Every character in Unicode has a lot of properties. They describe what "category" the character belongs to, contain miscellaneous information about it.
For instance, if a character has
Letter property, it means that the character belongs to an alphabet (of any language). And
Number property means that it's a digit: maybe Arabic or Chinese, and so on.
We can search for characters with a property, written as
pattern:\p{…}. To use
pattern:\p{…}, a regular expression must have flag
pattern:u.
For instance,
\p{Letter} denotes a letter in any language. We can also use
\p{L}, as
L is an alias of
Letter. There are shorter aliases for almost every property.
In the example below three kinds of letters will be found: English, Georgian and Korean.
run let str = "A ბ ㄱ";
alert( str.match(//gu) ); // A,ბ,ㄱ alert( str.match(//g) ); // null (no matches, doesn't work without the flag "u")
Here's the main character categories and their subcategories:
L:
Ll
Lm,
Lt,
Lu,
Lo.
N:
Nd,
Nl,
No.
P:
Pc,
Pd,
Pi,
Pf,
Ps,
Pe,
Po.
M (accents etc):
Mc,
Me,
Mn.
S:
Sc,
Sk,
Sm,
So.
Z:
Zl,
Zp,
Zs.
C:
Cc,
Cf,
Cn,
Co,
Cs.
So, e.g. if we need letters in lower case, we can write
pattern:\p{Ll}, punctuation signs:
pattern:\p{P} and so on.
There are also other derived categories, like: -
Alphabetic (
Alpha), includes Letters
L, plus letter numbers
Nl (e.g. Ⅻ - a character for the roman number 12), plus some other symbols
Other_Alphabetic (
OAlpha). -
Hex_Digit includes hexadecimal digits:
0-9,
a-f. - …And so on.
Unicode supports many different properties, their full list would require a lot of space, so here are the references:
For instance, let's look for hexadecimal numbers, written as
xFF, where
F is a hex digit (0..1 or A..F).
A hex digit can be denoted as
pattern:\p{Hex_Digit}:
run let regexp = /x/u;
alert("number: xAF".match(regexp)); // xAF
Let's look for Chinese hieroglyphs.
There's a Unicode property
Script (a writing system), that may have a value:
Cyrillic,
Greek,
Arabic,
Han (Chinese) and so on,
here's the full list.
To look for characters in a given writing system we should use
pattern:Script=<value>, e.g. for Cyrillic letters:
pattern:\p{sc=Cyrillic}, for Chinese hieroglyphs:
pattern:\p{sc=Han}, and so on:
run let regexp = //gu; // returns Chinese hieroglyphs
let str =
Hello Привет 你好 123_456;
alert( str.match(regexp) ); // 你,好
Characters that denote a currency, such as
$,
€,
¥, have Unicode property
pattern:\p{Currency_Symbol}, the short alias:
pattern:\p{Sc}.
Let's use it to look for prices in the format "currency, followed by a digit":
run let regexp = /gu;
let str =
Prices: $2, €1, ¥9;
alert( str.match(regexp) ); // $2,€1,¥9
Later, in the article info:regexp-quantifiers we'll see how to look for numbers that contain many digits.
Flag
pattern:u enables the support of Unicode in regular expressions.
That means two things:
\p{…}.
With Unicode properties we can look for words in given languages, special characters (quotes, currencies) and so on.
[recent browser="new"]
The nullish coalescing operator is written as two question marks
??.
As it treats
null and
undefined similarly, we'll use a special term here, in this article. We'll say that an expression is
"defined" when it's neither
null nor
undefined.
The result of
a ?? b is: - if
a is defined, then
a, - if
a isn't defined, then
b.
In other words,
?? returns the first argument if it's not
null/undefined. Otherwise, the second one.
The nullish coalescing operator isn't anything completely new. It's just a nice syntax to get the first "defined" value of the two.
We can rewrite
result = a ?? b using the operators that we already know, like this:
Now it should be absolutely clear what
?? does. Let's see where it helps.
The common use case for
?? is to provide a default value for a potentially undefined variable.
For example, here we show
user if defined, otherwise
Anonymous:
run let user;
alert(user ?? "Anonymous"); // Anonymous (user not defined)
Here's the example with
user assigned to a name:
run let user = "John";
alert(user ?? "Anonymous"); // John (user defined)
We can also use a sequence of
?? to select the first value from a list that isn't
null/undefined.
Let's say we have a user's data in variables
firstName,
lastName or
nickName. All of them may be not defined, if the user decided not to enter a value.
We'd like to display the user name using one of these variables, or show "Anonymous" if all of them aren't defined.
Let's use the
?? operator for that:
run let firstName = null; let lastName = null; let nickName = "Supercoder";
// shows the first defined value: ! alert(firstName ?? lastName ?? nickName ?? "Anonymous"); // Supercoder /!
The OR
|| operator can be used in the same way as
??, as it was described in the
previous chapter.
For example, in the code above we could replace
?? with
|| and still get the same result:
run let firstName = null; let lastName = null; let nickName = "Supercoder";
// shows the first truthy value: ! alert(firstName || lastName || nickName || "Anonymous"); // Supercoder /!
Historically, the OR
|| operator was there first. It exists since the beginning of JavaScript, so developers were using it
for such purposes for a long time.
On the other hand, the nullish coalescing operator
?? was added to JavaScript only recently, and the reason for that was that people weren't quite happy
with
||.
The important difference between them is that: -
|| returns the first
truthy value. -
?? returns the first
defined value.
In other words,
|| doesn't distinguish between
false,
0, an empty string
"" and
null/undefined. They are all the same - falsy values. If any of these is the first argument of
||, then we'll get the second argument as the result.
In practice though, we may want to use default value only when the variable is
null/undefined. That is, when the value is really unknown/not set.
For example, consider this:
run let height = 0;
alert(height || 100); // 100 alert(height ?? 100); // 0
height || 100 checks
height for being a falsy value, and it's
0, falsy indeed.
|| is the second argument,
100.
height ?? 100 checks
height for being
null/undefined, and it's not,
height "as is", that is
0.
In practice, the zero height is often a valid value, that shouldn't be replaced with the default. So
?? does just the right thing.
The precedence of the
?? operator is about the same as
||, just a bit lower. It equals
5 in the
MDN
table, while
|| is
6.
That means that, just like
||, the nullish coalescing operator
?? is evaluated before
= and
?, but after most other operations, such as
+,
*.
So if we'd like to choose a value with
?? in an expression with other operators, consider adding parentheses:
run let height = null; let width = null;
// important: use parentheses let area = (height ?? 100) * (width ?? 50);
alert(area); // 5000
Otherwise, if we omit parentheses, then as
* has the higher precedence than
??, it would execute first, leading to incorrect results.
// without parentheses
let area
= height
??
100
* width
??
50
;
// ...works the same as this (probably not what we want):
let area
= height
?? (
100
* width)
??
50
;
Due to safety reasons, JavaScript forbids using
?? together with
&& and
|| operators, unless the precedence is explicitly specified with parentheses.
The code below triggers a syntax error:
js run let x = 1 && 2 ?? 3; // Syntax error
The limitation is surely debatable, it was added to the language specification with the purpose to avoid
programming mistakes, when people start to switch from
|| to
??.
Use explicit parentheses to work around it:
run ! let x = (1 && 2) ?? 3; // Works /!
alert(x); // 2
The nullish coalescing operator
?? provides a short way to choose the first "defined" value from a list.
It's used to assign default values to variables:
?? has a very low precedence, only a bit higher than
? and
=, so consider adding parentheses when using it in an expression.
It's forbidden to use it with
|| or
&& without explicit parentheses.
The caret
pattern:^ and dollar
pattern:$ characters have special meaning in a regexp. They are called "anchors".
The caret
pattern:^ matches at the beginning of the text, and the dollar
pattern:$ - at the end.
For instance, let's test if the text starts with
Mary:
js run let str1 = "Mary had a little lamb"; alert( /^Mary/.test(str1) ); // true
The pattern
pattern:^Mary means: "string start and then Mary".
Similar to this, we can test if the string ends with
snow using
pattern:snow$:
js run let str1 = "it's fleece was white as snow"; alert( /snow$/.test(str1) ); // true
In these particular cases we could use string methods
startsWith/endsWith instead. Regular expressions should be used for more complex tests.
Both anchors together
pattern:^...$ are often used to test whether or not a string fully matches the pattern. For instance,
to check if the user input is in the right format.
Let's check whether or not a string is a time in
12:34 format. That is: two digits, then a colon, and then another two digits.
In regular expressions language that's
pattern:\d\d:\d\d:
run let goodInput = "12:34"; let badInput = "12:345";
let regexp = /^\d\d$/; alert( regexp.test(goodInput) ); // true alert( regexp.test(badInput) ); // false
Here the match for
pattern:\d\d:\d\d must start exactly after the beginning of the text
pattern:^, and the end
pattern:$ must immediately follow.
The whole string must be exactly in this format. If there's any deviation or an extra character, the result is
false.
Anchors behave differently if flag
pattern:m is present. We'll see that in the next article.
``
smart header="Anchors have \"zero width\"" Anchorspattern:^
andpattern:$` are tests. They have zero width.
In other words, they do not match a character, but rather force the regexp engine to check the condition (text start/end).
The multiline mode is enabled by the flag
pattern:m.
It only affects the behavior of
pattern:^ and
pattern:$.
In the multiline mode they match not only at the beginning and the end of the string, but also at start/end of line.
In the example below the text has multiple lines. The pattern
pattern:/^\d/gm takes a digit from the beginning of each line:
``
js run let str =1st place: Winnie 2nd place: Piglet 3rd place: Eeyore`;
! alert( str.match(/^gm) ); // 1, 2, 3 /!
Without the flag
pattern:m only the first digit is matched:
``
js run let str =1st place: Winnie 2nd place: Piglet 3rd place: Eeyore`;
! alert( str.match(/^g) ); // 1 /!
That's because by default a caret
pattern:^ only matches at the beginning of the text, and in the multiline mode - at the start of any
line.
"Start of a line" formally means "immediately after a line break": the test `pattern:^` in multiline mode matches at all positions preceded by a newline character `\n`.
And at the text start.
The dollar sign
pattern:$ behaves similarly.
The regular expression
pattern:\d$ finds the last digit in every line
``
js run let str =Winnie: 1 Piglet: 2 Eeyore: 3`;
alert( str.match(/\d$/gm) ); // 1,2,3
Without the flag
pattern:m, the dollar
pattern:$ would only match the end of the whole text, so only the very last digit would be found.
"End of a line" formally means "immediately before a line break": the test `pattern:$` in multiline mode matches at all positions succeeded by a newline character `\n`.
And at the text end.
To find a newline, we can use not only anchors
pattern:^ and
pattern:$, but also the newline character
\n.
What's the difference? Let's see an example.
Here we search for
pattern:\d\n instead of
pattern:\d$:
``
js run let str =Winnie: 1 Piglet: 2 Eeyore: 3`;
alert( str.match(//gm) ); // 1,2
As we can see, there are 2 matches instead of 3.
That's because there's no newline after
subject:3 (there's text end though, so it matches
pattern:$).
Another difference: now every match includes a newline character
match:\n. Unlike the anchors
pattern:^
pattern:$, that only test the condition (start/end of a line),
\n is a character, so it becomes a part of the result.
So, a
\n in the pattern is used when we need newline characters in the result, while anchors are used to
find something at the beginning/end of a line.
pattern:\b is a test, just like
pattern:^ and
pattern:$.
When the regexp engine (program module that implements searching for regexps) comes across
pattern:\b, it checks that the position in the string is a word boundary.
There are three different positions that qualify as word boundaries:
pattern:\w.
pattern:\w and the other is not.
pattern:\w.
For instance, regexp
pattern:\bJava\b will be found in
subject:Hello, Java!, where
subject:Java is a standalone word, but not in
subject:Hello, JavaScript!.
js run alert( "Hello, Java!".match(/\bJava\b/) ); // Java alert( "Hello, JavaScript!".match(/\bJava\b/) ); // null
In the string
subject:Hello, Java! following positions correspond to
pattern:\b:
So, it matches the pattern
pattern:\bHello\b, because:
pattern:\b.
pattern:Hello.
pattern:\b matches again, as we're between
subject:o and a comma.
So the pattern
pattern:\bHello\b would match, but not
pattern:\bHell\b (because there's no word boundary after
l) and not
Java!\b (because the exclamation sign is not a wordly character
pattern:\w, so there's no word boundary after it).
js run alert( "Hello, Java!".match(/\bHello\b/) ); // Hello alert( "Hello, Java!".match(/\bJava\b/) ); // Java alert( "Hello, Java!".match(/\bHell\b/) ); // null (no match) alert( "Hello, Java!".match(/\bJava!\b/) ); // null (no match)
We can use
pattern:\b not only with words, but with digits as well.
For example, the pattern
pattern:\b\d\d\b looks for standalone 2-digit numbers. In other words, it looks for 2-digit numbers
that are surrounded by characters different from
pattern:\w, such as spaces or punctuation (or text start/end).
js run alert( "1 23 456 78".match(/\b\d\d\b/g) ); // 23,78 alert( "12,34,56".match(/\b\d\d\b/g) ); // 12,34,56
``
warn header="Word boundarypattern: doesn't work for non-latin alphabets" The word boundary test
pattern:\b checks that there should be
pattern:\w on the one side from the position and "not
pattern:\w" - on the other side.
But
pattern:\w means a latin letter
a-z (or a digit or an underscore), so the test doesn't work for other characters, e.g. cyrillic
letters or hieroglyphs.
As we've seen, a backslash
pattern:\ is used to denote character classes, e.g.
pattern:\d. So it's a special character in regexps (just like in regular strings).
There are other special characters as well, that have special meaning in a regexp. They are used to do more
powerful searches. Here's a full list of them:
pattern:[ \ ^ $ . | ? * + ( ).
Don't try to remember the list - soon we'll deal with each of them separately and you'll know them by heart automatically.
Let's say we want to find literally a dot. Not "any character", but just a dot.
To use a special character as a regular one, prepend it with a backslash:
pattern:\..
That's also called "escaping a character".
For example:
js run alert( "Chapter 5.1".match(/\d\.\d/) ); // 5.1 (match!) alert( "Chapter 511".match(/\d\.\d/) ); // null (looking for a real dot \.)
Parentheses are also special characters, so if we want them, we should use
pattern:\(. The example below looks for a string
"g()":
js run alert( "function g()".match(/g\(\)/) ); // "g()"
If we're looking for a backslash
\, it's a special character in both regular strings and regexps, so we should double it.
js run alert( "1\\2".match(/\\/) ); // '\'
A slash symbol
'/' is not a special character, but in JavaScript it is used to open and close the regexp:
pattern:/...pattern.../, so we should escape it too.
Here's what a search for a slash
'/' looks like:
js run alert( "/".match(/\//) ); // '/'
On the other hand, if we're not using
pattern:/.../, but create a regexp using
new RegExp, then we don't need to escape it:
js run alert( "/".match(new RegExp("/")) ); // finds /
If we are creating a regular expression with
new RegExp, then we don't have to escape
/, but need to do some other escaping.
For instance, consider this:
run let regexp = new RegExp(");
alert( "Chapter 5.1".match(regexp) ); // null
The similar search in one of previous examples worked with
pattern:/\d\.\d/, but
new RegExp("\d\.\d") doesn't work, why?
The reason is that backslashes are "consumed" by a string. As we may recall, regular strings have their own
special characters, such as
\n, and a backslash is used for escaping.
Here's how " is preceived:
js run alert("\d\.\d"); // d.d
String quotes "consume" backslashes and interpret them on their own, for instance:
\n - becomes a newline character,
\u1234 - becomes the Unicode character with such code,
pattern:\d or
\z, then the backslash is simply removed.
So
new RegExp gets a string without backslashes. That's why the search doesn't work!
To fix it, we need to double backslashes, because string quotes turn
\\ into
\:
run ! let regStr = "\d\.\d"; /! alert(regStr); // correct now)
let regexp = new RegExp(regStr);
alert( "Chapter 5.1".match(regexp) ); // 5.1
pattern:[ \ ^ $ . | ? * + ( ) literally, we need to prepend them with a backslash
\ ("escape them").
/ if we're inside
pattern:/.../ (but not inside
new RegExp).
new RegExp, we need to double backslashes
\\, cause string quotes consume one of them.
Several characters or character classes inside square brackets
[…] mean to "search for any character among given".
For instance,
pattern:[eao] means any of the 3 characters:
'a',
'e', or
'o'.
That's called a set. Sets can be used in a regexp along with regular characters:
js run // find [t or m], and then "op" alert( "Mop top".match(/[tm]op/gi) ); // "Mop", "top"
Please note that although there are multiple characters in the set, they correspond to exactly one character in the match.
So the example below gives no matches:
js run // find "V", then [o or i], then "la" alert( "Voila".match(/V[oi]la/) ); // null, no matches
The pattern searches for:
pattern:V,
pattern:[oi],
pattern:la.
So there would be a match for
match:Vola or
match:Vila.
Square brackets may also contain character ranges.
For instance,
pattern:[a-z] is a character in range from
a to
z, and
pattern:[0-5] is a digit from
0 to
5.
In the example below we're searching for
"x" followed by two digits or letters from
A to
F:
js run alert( "Exception 0xAF".match(/x[0-9A-F][0-9A-F]/g) ); // xAF
Here
pattern:[0-9A-F] has two ranges: it searches for a character that is either a digit from
0 to
9 or a letter from
A to
F.
If we'd like to look for lowercase letters as well, we can add the range
a-f:
pattern:[0-9A-Fa-f]. Or add the flag
pattern:i.
We can also use character classes inside
[…].
For instance, if we'd like to look for a wordly character
pattern:\w or a hyphen
pattern:-, then the set is
pattern:[\w-].
Combining multiple classes is also possible, e.g.
pattern:[\s\d] means "a space character or a digit".
smart header="Character classes are shorthands for certain character sets" For instance:
pattern:[0-9],
pattern:[a-zA-Z0-9_],
pattern:[\t\n\v\f\r ], plus few other rare Unicode space characters.
As the character class
pattern:\w is a shorthand for
pattern:[a-zA-Z0-9_], it can't find Chinese hieroglyphs, Cyrillic letters, etc.
We can write a more universal pattern, that looks for wordly characters in any language. That's easy with Unicode
properties:
pattern:[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}].
Let's decipher it. Similar to
pattern:\w, we're making a set of our own that includes characters with following Unicode properties:
Alphabetic (
Alpha) - for letters,
Mark (
M) - for accents,
Decimal_Number (
Nd) - for digits,
Connector_Punctuation (
Pc) - for the underscore
'_' and similar characters,
Join_Control (
Join_C) - two special codes
200c and
200d, used in ligatures, e.g. in Arabic.
An example of use:
run let regexp = /[]/gu;
let str =
Hi 你好 12;
// finds all letters and digits: alert( str.match(regexp) ); // H,i,你,好,1,2
Of course, we can edit this pattern: add Unicode properties or remove them. Unicode properties are covered in more details in the article info:regexp-unicode.
``
warn header="Unicode properties aren't supported in IE" Unicode propertiespattern:p{…}` are not
implemented in IE. If we really need them, we can use library
XRegExp.
Or just use ranges of characters in a language that interests us, e.g.
pattern:[а-я] for Cyrillic letters.
Besides normal ranges, there are "excluding" ranges that look like
pattern:[^…].
They are denoted by a caret character
^ at the start and match any character
except the given ones.
For instance:
pattern:[^aeyo] - any character except
'a',
'e',
'y' or
'o'.
pattern:[^0-9] - any character except a digit, the same as
pattern:\D.
pattern:[^\s] - any non-space character, same as
\S.
The example below looks for any characters except letters, digits and spaces:
js run alert( "alice15@gmail.com".match(/[^\d\sA-Z]/gi) ); // @ and .
Usually when we want to find exactly a special character, we need to escape it like
pattern:\.. And if we need a backslash, then we use
pattern:\\, and so on.
In square brackets we can use the vast majority of special characters without escaping:
pattern:. + ( ) never need escaping.
pattern:- is not escaped in the beginning or the end (where it does not define a range).
pattern:^ is only escaped in the beginning (where it means exclusion).
pattern:] is always escaped (if we need to look for that symbol).
In other words, all special characters are allowed without escaping, except when they mean something for square brackets.
A dot
. inside square brackets means just a dot. The pattern
pattern:[.,] would look for one of characters: either a dot or a comma.
In the example below the regexp
pattern:[-().^+] looks for one of the characters
-().^+:
run // No need to escape let regexp = /[-().^+]/g;
alert( "1 + 2 - 3".match(regexp) ); // Matches +, -
…But if you decide to escape them "just in case", then there would be no harm:
run // Escaped everything let regexp = /[-().^+]/g;
alert( "1 + 2 - 3".match(regexp) ); // also works: +, -
If there are surrogate pairs in the set, flag
pattern:u is required for them to work correctly.
For instance, let's look for
pattern:[𝒳𝒴] in the string
subject:𝒳:
js run alert( '𝒳'.match(/[𝒳𝒴]/) ); // shows a strange character, like [?] // (the search was performed incorrectly, half-character returned)
The result is incorrect, because by default regular expressions "don't know" about surrogate pairs.
The regular expression engine thinks that
[𝒳𝒴] - are not two, but four characters: 1. left half of
𝒳
(1), 2. right half of
𝒳
(2), 3. left half of
𝒴
(3), 4. right half of
𝒴
(4).
We can see their codes like this:
js run for(let i=0; i<'𝒳𝒴'.length; i++) { alert('𝒳𝒴'.charCodeAt(i)); // 55349, 56499, 55349, 56500 };
So, the example above finds and shows the left half of
𝒳.
If we add flag
pattern:u, then the behavior will be correct:
js run alert( '𝒳'.match(/[𝒳𝒴]/u) ); // 𝒳
The similar situation occurs when looking for a range, such as
[𝒳-𝒴].
If we forget to add flag
pattern:u, there will be an error:
js run '𝒳'.match(/[𝒳-𝒴]/); // Error: Invalid regular expression
The reason is that without flag
pattern:u surrogate pairs are perceived as two characters, so
[𝒳-𝒴] is interpreted as
[<55349><56499>-<55349><56500>] (every surrogate pair is replaced with its
codes). Now it's easy to see that the range
56499-55349 is invalid: its starting code
56499 is greater than the end
55349. That's the formal reason for the error.
With the flag
pattern:u the pattern works correctly:
js run // look for characters from 𝒳 to 𝒵 alert( '𝒴'.match(/[𝒳-𝒵]/u) ); // 𝒴
Let's say we have a string like
+7(903)-123-45-67 and want to find all numbers in it. But unlike before, we are interested not in
single digits, but full numbers:
7, 903, 123, 45, 67.
A number is a sequence of 1 or more digits
pattern:\d. To mark how many we need, we can append a
quantifier.
The simplest quantifier is a number in curly braces:
pattern:{n}.
A quantifier is appended to a character (or a character class, or a
[...] set etc) and specifies how many we need.
It has a few advanced forms, let's see examples:
pattern:{5}
pattern:\d{5} denotes exactly 5 digits, the same as
pattern:\d\d\d\d\d.
The example below looks for a 5-digit number:
js run alert( "I'm 12345 years old".match(/\d{5}/) ); // "12345"
We can add
\b to exclude longer numbers:
pattern:\b\d{5}\b.
pattern:{3,5}, match 3-5 times
To find numbers from 3 to 5 digits we can put the limits into curly braces:
pattern:\d{3,5}
js run alert( "I'm not 12, but 1234 years old".match(/\d{3,5}/) ); // "1234"
We can omit the upper limit.
Then a regexp
pattern:\d{3,} looks for sequences of digits of length
3 or more:
js run alert( "I'm not 12, but 345678 years old".match(/\d{3,}/) ); // "345678"
Let's return to the string
+7(903)-123-45-67.
A number is a sequence of one or more digits in a row. So the regexp is
pattern:\d{1,}:
run let str = "+7(903)-123-45-67";
let numbers = str.match(//g);
alert(numbers); // 7,903,123,45,67
There are shorthands for most used quantifiers:
pattern:+
Means "one or more", the same as
pattern:{1,}.
For instance,
pattern:\d+ looks for numbers:
run let str = "+7(903)-123-45-67";
alert( str.match(//g) ); // 7,903,123,45,67
pattern:?
Means "zero or one", the same as
pattern:{0,1}. In other words, it makes the symbol optional.
For instance, the pattern
pattern:ou?r looks for
match:o followed by zero or one
match:u, and then
match:r.
So,
pattern:colou?r finds both
match:color and
match:colour:
run let str = "Should I write color or colour?";
alert( str.match(/colou?r/g) ); // color, colour
pattern:*
Means "zero or more", the same as
pattern:{0,}. That is, the character may repeat any times or be absent.
For example,
pattern:\d0* looks for a digit followed by any number of zeroes (may be many or none):
js run alert( "100 10 1".match(/\d0*/g) ); // 100, 10, 1
Compare it with
pattern:+ (one or more):
js run alert( "100 10 1".match(/\d0+/g) ); // 100, 10 // 1 not matched, as 0+ requires at least one zero
Quantifiers are used very often. They serve as the main "building block" of complex regular expressions, so let's see more examples.
Regexp for decimal fractions (a number with a floating point):
pattern:\d+\.\d+
In action:
js run alert( "0 1 12.345 7890".match(/\d+\.\d+/g) ); // 12.345
Regexp for an "opening HTML-tag without attributes", such as
<span> or
<p>.
The simplest one:
pattern:/<[a-z]+>/i
js run alert( "<body> ... </body>".match(/<[a-z]+>/gi) ); // <body>
The regexp looks for character
pattern:'<' followed by one or more Latin letters, and then
pattern:'>'.
Improved:
pattern:/<[a-z][a-z0-9]*>/i
According to the standard, HTML tag name may have a digit at any position except the first one, like
<h1>.
js run alert( "<h1>Hi!</h1>".match(/<[a-z][a-z0-9]*>/gi) ); // <h1>
Regexp "opening or closing HTML-tag without attributes":
pattern:/<\/?[a-z][a-z0-9]*>/i
We added an optional slash
pattern:/? near the beginning of the pattern. Had to escape it with a backslash, otherwise JavaScript
would think it is the pattern end.
js run alert( "<h1>Hi!</h1>".match(/<\/?[a-z][a-z0-9]*>/gi) ); // <h1>, </h1>
smart header="To make a regexp more precise, we often need make it more complex" We can see one common rule in these examples: the more precise is the regular expression - the longer and more complex it is.
For instance, for HTML tags we could use a simpler regexp:
pattern:<\w+>. But as HTML has stricter restrictions for a tag name,
pattern:<[a-z][a-z0-9]*> is more reliable.
Can we use
pattern:<\w+> or we need
pattern:<[a-z][a-z0-9]*>?
In real life both variants are acceptable. Depends on how tolerant we can be to "extra" matches and whether it's difficult or not to remove them from the result by other means.
Quantifiers are very simple from the first sight, but in fact they can be tricky.
We should understand how the search works very well if we plan to look for something more complex than
pattern:/\d+/.
Let's take the following task as an example.
We have a text and need to replace all quotes
"..." with guillemet marks:
«...». They are preferred for typography in many countries.
For instance:
"Hello, world" should become
«Hello, world». There exist other quotes, such as
„Witam, świat!" (Polish) or
「你好,世界」 (Chinese), but for our task let's choose
«...».
The first thing to do is to locate quoted strings, and then we can replace them.
A regular expression like
pattern:/".+"/g (a quote, then something, then the other quote) may seem like a good fit, but it
isn't!
Let's try it:
run let regexp = /".+"/g;
let str = ‘a "witch" and her "broom" is one';
alert( str.match(regexp) ); // "witch" and her "broom"
…We can see that it works not as intended!
Instead of finding two matches
match:"witch" and
match:"broom", it finds one:
match:"witch" and her "broom".
That can be described as "greediness is the cause of all evil".
To find a match, the regular expression engine uses the following algorithm:
These common words do not make it obvious why the regexp fails, so let's elaborate how the search works for the
pattern
pattern:".+".
The first pattern character is a quote
pattern:".
The regular expression engine tries to find it at the zero position of the source string
subject:a "witch" and her "broom" is one, but there's
subject:a there, so there's immediately no match.
Then it advances: goes to the next positions in the source string and tries to find the first character of the pattern there, fails again, and finally finds the quote at the 3rd position:
The quote is detected, and then the engine tries to find a match for the rest of the pattern. It tries to see
if the rest of the subject string conforms to
pattern:.+".
In our case the next pattern character is
pattern:. (a dot). It denotes "any character except a newline", so the next string letter
match:'w' fits:
Then the dot repeats because of the quantifier
pattern:.+. The regular expression engine adds to the match one character after another.
…Until when? All characters match the dot, so it only stops when it reaches the end of the string:
Now the engine finished repeating
pattern:.+ and tries to find the next character of the pattern. It's the quote
pattern:". But there's a problem: the string has finished, there are no more characters!
The regular expression engine understands that it took too many
pattern:.+ and starts to
backtrack.
In other words, it shortens the match for the quantifier by one character:
Now it assumes that
pattern:.+ ends one character before the string end and tries to match the rest of the pattern
from that position.
If there were a quote there, then the search would end, but the last character is
subject:'e', so there's no match.
…So the engine decreases the number of repetitions of
pattern:.+ by one more character:
The quote
pattern:'"' does not match
subject:'n'.
The engine keep backtracking: it decreases the count of repetition for
pattern:'.' until the rest of the pattern (in our case
pattern:'"') matches:
The match is complete.
So the first match is
match:"witch" and her "broom". If the regular expression has flag
pattern:g, then the search will continue from where the first match ends. There are no more
quotes in the rest of the string
subject:is one, so no more results.
That's probably not what we expected, but that's how it works.
In the greedy mode (by default) a quantified character is repeated as many times as possible.
The regexp engine adds to the match as many characters as it can for
pattern:.+, and then shortens that one by one, if the rest of the pattern doesn't match.
For our task we want another thing. That's where a lazy mode can help.
The lazy mode of quantifiers is an opposite to the greedy mode. It means: "repeat minimal number of times".
We can enable it by putting a question mark
pattern:'?' after the quantifier, so that it becomes
pattern:*? or
pattern:+? or even
pattern:?? for
pattern:'?'.
To make things clear: usually a question mark
pattern:? is a quantifier by itself (zero or one), but if added
after another quantifier (or even itself) it gets another meaning - it switches the matching mode from
greedy to lazy.
The regexp
pattern:/".+?"/g works as intended: it finds
match:"witch" and
match:"broom":
run let regexp = /".+?"/g;
let str = ‘a "witch" and her "broom" is one';
alert( str.match(regexp) ); // "witch", "broom"
To clearly understand the change, let's trace the search step by step.
The first step is the same: it finds the pattern start
pattern:'"' at the 3rd position:
The next step is also similar: the engine finds a match for the dot
pattern:'.':
And now the search goes differently. Because we have a lazy mode for
pattern:+?, the engine doesn't try to match a dot one more time, but stops and tries to match the
rest of the pattern
pattern:'"' right now:
'i', so there's no match.
Then the regular expression engine increases the number of repetitions for the dot and tries one more time:
…Till the match for the rest of the pattern is found:
The next search starts from the end of the current match and yield one more result:
In this example we saw how the lazy mode works for
pattern:+?. Quantifiers
pattern:*? and
pattern:?? work the similar way - the regexp engine increases the number of repetitions only if the
rest of the pattern can't match on the given position.
Laziness is only enabled for the quantifier with
?.
Other quantifiers remain greedy.
For instance:
js run alert( "123 456".match(/\d+ \d+?/) ); // 123 4
pattern:\d+ tries to match as many digits as it can (greedy mode), so it finds
match:123 and stops, because the next character is a space
pattern:' '.
Then there's
pattern:\d+?. The quantifier is in lazy mode, so it finds one digit
match:4 and tries to check if the rest of the pattern matches from there.
…But there's nothing in the pattern after
pattern:\d+?.
The lazy mode doesn't repeat anything without a need. The pattern finished, so we're done. We have a match
match:123 4.
smart header="Optimizations" Modern regular expression engines can optimize internal algorithms to work faster. So they may work a bit differently from the described algorithm.
But to understand how regular expressions work and to build regular expressions, we don't need to know about that. They are only used internally to optimize things.
Complex regular expressions are hard to optimize, so the search may work exactly as described as well.
With regexps, there's often more than one way to do the same thing.
In our case we can find quoted strings without lazy mode using the regexp
pattern:"[^"]+":
run let regexp = /"[^"]+"/g;
let str = ‘a "witch" and her "broom" is one';
alert( str.match(regexp) ); // "witch", "broom"
The regexp
pattern:"[^"]+" gives correct results, because it looks for a quote
pattern:'"' followed by one or more non-quotes
pattern:[^"], and then the closing quote.
When the regexp engine looks for
pattern:[^"]+ it stops the repetitions when it meets the closing quote, and we're done.
Please note, that this logic does not replace lazy quantifiers!
It is just different. There are times when we need one or another.
Let's see an example where lazy quantifiers fail and this variant works right.
For instance, we want to find links of the form
<a href="..." class="doc">, with any
href.
Which regular expression to use?
The first idea might be:
pattern:/<a href=".*" class="doc">/g.
Let's check it: run let str = ‘… …'; let regexp = / /g;
// Works! alert( str.match(regexp) ); //
It worked. But let's see what happens if there are many links in the text?
run let str = ‘… … …'; let regexp = / /g;
// Whoops! Two links in one match! alert( str.match(regexp) ); // …
Now the result is wrong for the same reason as our "witches" example. The quantifier
pattern:.* took too many characters.
The match looks like this:
<a
href=
"....................................."
class=
"doc"
>
<a
href=
"link1"
class=
"doc"
>...
<a
href=
"link2"
class=
"doc"
>
Let's modify the pattern by making the quantifier
pattern:.*? lazy:
run let str = ‘… … …'; let regexp = / /g;
// Works! alert( str.match(regexp) ); // ,
Now it seems to work, there are two matches:
<a
href=
"....."
class=
"doc"
>
<a
href=
"....."
class=
"doc"
>
<a
href=
"link1"
class=
"doc"
>...
<a
href=
"link2"
class=
"doc"
>
…But let's test it on one more text input:
run let str = '… …
…'; let regexp = / /g;
// Wrong match! alert( str.match(regexp) ); // …
Now it fails. The match includes not just a link, but also a lot of text after it, including
<p...>.
Why?
That's what's going on:
match:<a href=".
pattern:.*?: takes one character (lazily!), check if there's a match for
pattern:" class="doc"> (none).
pattern:.*?, and so on… until it finally reaches
match:" class="doc">.
But the problem is: that's already beyond the link
<a...>, in another tag
<p>. Not what we want.
Here's the picture of the match aligned with the text:
<a
href=
"..................................."
class=
"doc"
>
<a
href=
"link1"
class=
"wrong"
>...
<p
style=
""
class=
"doc"
>
So, we need the pattern to look for
<a href="...something..." class="doc">, but both greedy and lazy variants have
problems.
The correct variant can be:
pattern:href="[^"]*". It will take all characters inside the
href attribute till the nearest quote, just what we need.
A working example:
run let str1 = '… …
…‘; let str2 ='… … …'; let regexp = /<a href="[^"]*" class="doc">/g;
// Works! alert( str1.match(regexp) ); // null, no matches, that's correct alert( str2.match(regexp) ); // ,
Quantifiers have two modes of work:
pattern:\d+ consumes all possible digits. When it becomes impossible to consume more
(no more digits or string end), then it continues to match the rest of the pattern. If there's no
match then it decreases the number of repetitions (backtracks) and tries again.
pattern:? after the quantifier. The regexp engine tries to match the rest of the
pattern before each repetition of the quantified character.
As we've seen, the lazy mode is not a "panacea" from the greedy search. An alternative is a
"fine-tuned" greedy search, with exclusions, as in the pattern
pattern:"[^"]+".
A part of a pattern can be enclosed in parentheses
pattern:(...). This is called a "capturing group".
That has two effects:
Let's see how parentheses work in examples.
Without parentheses, the pattern
pattern:go+ means
subject:g character, followed by
subject:o repeated one or more times. For instance,
match:goooo or
match:gooooooooo.
Parentheses group characters together, so
pattern:(go)+ means
match:go,
match:gogo,
match:gogogo and so on.
js run alert( 'Gogogo now!'.match(/(go)+/ig) ); // "Gogogo"
Let's make something more complex - a regular expression to search for a website domain.
For example:
mail.com
users.mail.com
smith.users.mail.com
As we can see, a domain consists of repeated words, a dot after each one except the last one.
In regular expressions that's
pattern:(\w+\.)+\w+:
run let regexp = /(+.)++/g;
alert( "site.com my.site.com".match(regexp) ); // site.com,my.site.com
The search works, but the pattern can't match a domain with a hyphen, e.g.
my-site.com, because the hyphen does not belong to class
pattern:\w.
We can fix it by replacing
pattern:\w with
pattern:[\w-] in every word except the last one:
pattern:([\w-]+\.)+\w+.
The previous example can be extended. We can create a regular expression for emails based on it.
The email format is:
name@domain. Any word can be the name, hyphens and dots are allowed. In regular
expressions that's
pattern:[-.\w]+.
The pattern:
run let regexp = /[-.]+@([-]+.)+[-]+/g;
alert("my@mail.com @ his@site.com.uk".match(regexp)); // my@mail.com, his@site.com.uk
That regexp is not perfect, but mostly works and helps to fix accidental mistypes. The only truly reliable check for an email can only be done by sending a letter.
Parentheses are numbered from left to right. The search engine memorizes the content matched by each of them and allows to get it in the result.
The method
str.match(regexp), if
regexp has no flag
g, looks for the first match and returns it as an array:
0: the full match.
1: the contents of the first parentheses.
2: the contents of the second parentheses.
For instance, we'd like to find HTML tags
pattern:<.*?>, and process them. It would be convenient to have tag content (what's
inside the angles), in a separate variable.
Let's wrap the inner content into parentheses, like this:
pattern:<(.*?)>.
Now we'll get both the tag as a whole
match:<h1> and its contents
match:h1 in the resulting array:
''';
let tag = str.match(/<(.*?)>/);
alert( tag[0] ); //alert( tag[1] ); // h1
Parentheses can be nested. In this case the numbering also goes from left to right.
For instance, when searching a tag in
subject:<span class="my"> we may be interested in:
match:span class="my".
match:span.
match:class="my".
Let's add parentheses for them:
pattern:<(([a-z]+)\s*([^>]*))>.
Here's how they are numbered (left to right, by the opening paren):
In action:
run let str = ‘ ''';
let regexp = /<(([a-z]+)([^>]*))>/;
let result = str.match(regexp); alert(result[0]); // alert(result[1]); // span class="my" alert(result[2]); // span alert(result[3]); // class="my"
The zero index of
result always holds the full match.
Then groups, numbered from left to right by an opening paren. The first group is returned as
result[1]. Here it encloses the whole tag content.
Then in
result[2] goes the group from the second opening paren
pattern:([a-z]+) - tag name, then in
result[3] the tag:
pattern:([^>]*).
The contents of every group in the string:
Even if a group is optional and doesn't exist in the match (e.g. has the quantifier
pattern:(...)?), the corresponding
result array item is present and equals
undefined.
For instance, let's consider the regexp
pattern:a(z)?(c)?. It looks for
"a" optionally followed by
"z" optionally followed by
"c".
If we run it on the string with a single letter
subject:a, then the result is:
run let match = ‘a'.match(/a(z)?(c)?/);
alert( match.length ); // 3 alert( match[0] ); // a (whole match) alert( match[1] ); // undefined alert( match[2] ); // undefined
The array has the length of
3, but all groups are empty.
And here's a more complex match for the string
subject:ac:
run let match = ‘ac'.match(/a(z)?(c)?/)
alert( match.length ); // 3 alert( match[0] ); // ac (whole match) alert( match[1] ); // undefined, because there's nothing for (z)? alert( match[2] ); // c
The array length is permanent:
3. But there's nothing for the group
pattern:(z)?, so the result is
["ac", undefined, "c"].
``
warn header="matchAll
is a new method, polyfill may be needed" The methodmatchAll` is not supported in old
browsers.
A polyfill may be required, such as https://github.com/ljharb/String.prototype.matchAll.
When we search for all matches (flag
pattern:g), the
match method does not return contents for groups.
For example, let's find all tags in a string:
run let str = '''''';
let tags = str.match(/<(.*?)>/g);
alert( tags ); //
The result is an array of matches, but without details about each of them. But in practice we usually need contents of capturing groups in the result.
To get them, we should search using the method
undefinedundefinedstr.matchAll(regexp).
It was added to JavaScript language long after undefinedundefinedmatch, as its
"new and improved version".
Just like undefinedundefinedmatch, it looks for matches, but there are 3
differences:
pattern:g is present, it
returns every match as an array with groups.undefinedundefinednull, but an empty
iterable object.undefinedundefinedFor instance:
run let results = ''''''.matchAll(/<(.*?)>/gi);
undefinedundefined// results - is not an array, but an iterable object alert(results); // [object RegExp String Iterator]
undefinedundefinedalert(results[0]); // undefined (*)
undefinedundefinedresults = Array.from(results); // let's turn it into array
alert(results[0]); // undefinedundefined,h2 (2nd tag)
undefinedundefinedAs we can see, the first difference is very important, as
demonstrated in the line undefinedundefined(*). We can't get the match
as undefinedundefinedresults[0], because that object isn't pseudoarray.
We can turn it into a real undefinedundefinedArray using
undefinedundefinedArray.from. There are more details about pseudoarrays
and iterables in the article undefinedundefinedinfo:iterable.undefinedundefined
There's
no need in undefinedundefinedArray.from if we're looping over
results:undefinedundefined
'''.matchAll(/<(.*?)>/gi);
for(let result of results) { alert(result); // first alert: undefinedundefined,h2 }
undefinedundefined…Or using destructuring:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet [tag1undefinedundefined, tag2] undefinedundefined=undefinedundefined'<h1> <h2>'.undefinedundefinedmatchAll(undefinedundefined/<undefinedundefined(undefinedundefined.undefinedundefined*?)undefinedundefined>/gi)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Every match, returned by
undefinedundefinedmatchAll, has the same format as returned by
undefinedundefinedmatch without flag
undefinedundefinedpattern:g: it's an array with additional
properties undefinedundefinedindex (match index in the string)
and undefinedundefinedinput (source string):undefinedundefined
'''.matchAll(/<(.*?)>/gi);
undefinedundefinedlet [tag1, tag2] = results;
alert( tag1[0] ); // undefinedundefinedundefinedundefined
``undefinedundefinedsmart header="Why is a result ofmatchAll`
an iterable object, not an array?" Why is the method designed like
that? The reason is simple - for the
optimization.undefinedundefined
The call
to undefinedundefinedmatchAll does not perform the
search. Instead, it returns an iterable object, without the
results initially. The search is performed each time we iterate
over it, e.g. in the loop.undefinedundefined
So, there will be found as many results as needed, not more.
undefinedundefinedE.g. there are potentially 100 matches in the
text, but in a undefinedundefinedfor..of loop we
found 5 of them, then decided it's enough and made a
undefinedundefinedbreak. Then the engine won't spend
time finding other 95 matches.
undefinedundefined
Remembering groups by their numbers is hard. For simple patterns it's doable, but for more complex ones counting parentheses is inconvenient. We have a much better option: give names to parentheses.
undefinedundefined
That's done by putting
undefinedundefinedpattern:?<name> immediately
after the opening paren.undefinedundefined
For example, let's look for a date in the format "year-month-day":
undefinedundefined run
undefinedundefined! let dateRegexp =
/(?undefinedundefined
let groups = str.match(dateRegexp).groups;
undefinedundefinedalert(groups.year); // 2019 alert(groups.month); // 04 alert(groups.day); // 30
undefinedundefinedAs you can see, the groups reside in the
undefinedundefined.groups property of the
match.undefinedundefined
To look for all
dates, we can add flag
undefinedundefinedpattern:g.undefinedundefined
We'll also need
undefinedundefinedmatchAll to obtain full matches,
together with groups:undefinedundefined
run let dateRegexp = /(?undefinedundefined
let str = "2019-10-30 2020-01-01";
undefinedundefinedlet results = str.matchAll(dateRegexp);
undefinedundefinedfor(let result of results) { let {year, month, day} = result.groups;
undefinedundefined
alert(undefinedundefined${day}.${month}.${year}); //
first alert: 30.10.2019 // second: 01.01.2020 }
undefinedundefined
Method
undefinedundefinedstr.replace(regexp, replacement)
that replaces all matches with
undefinedundefinedregexp in
undefinedundefinedstr allows to use parentheses
contents in the undefinedundefinedreplacement string.
That's done using undefinedundefinedpattern:$n, where
undefinedundefinedpattern:n is the group
number.undefinedundefined
For example,
undefinedundefinedrun let str = "John Bull"; let regexp = /(+) (+)/;
undefinedundefinedalert( str.replace(regexp, ‘$2, $1') ); // Bull, John
undefinedundefinedFor named parentheses the reference will be
undefinedundefinedpattern:$<name>.undefinedundefined
For example, let's reformat dates from "year-month-day" to "day.month.year":
undefinedundefined run
let regexp = /(?undefinedundefined
let str = "2019-10-30, 2020-01-01";
undefinedundefinedalert(
str.replace(regexp, ‘undefinedundefined < undefinedundefineddundefinedundefinedaundefinedundefinedy > .undefinedundefinedundefinedundefined
Sometimes we need parentheses to correctly apply a quantifier, but we don't want their contents in results.
undefinedundefinedA group may be excluded by adding
undefinedundefinedpattern:?: in the
beginning.undefinedundefined
For instance,
if we want to find undefinedundefinedpattern:(go)+,
but don't want the parentheses contents
(undefinedundefinedgo) as a separate array item, we
can write:
undefinedundefinedpattern:(?:go)+.undefinedundefined
In the example below we only get the name
undefinedundefinedmatch:John as a separate member of
the match:undefinedundefined
run let str = "Gogogo John!";
undefinedundefinedundefinedundefined! // ?: exludes ‘go' from capturing let regexp = /(?:go)+ (+)/i; undefinedundefined/!undefinedundefined
undefinedundefinedlet result = str.match(regexp);
undefinedundefinedalert( result[0] ); // Gogogo John (full match) alert( result[1] ); // John alert( result.length ); // 2 (no more items in the array)
undefinedundefinedParentheses group together a part of the regular expression, so that the quantifier applies to it as a whole.
undefinedundefinedParentheses groups are numbered
left-to-right, and can optionally be named with
undefinedundefined(?<name>...).undefinedundefined
The content, matched by a group, can be obtained in the results:
undefinedundefinedstr.match returns capturing
groups only without flag
undefinedundefinedpattern:g.undefinedundefinedstr.matchAll always returns
capturing groups.undefinedundefinedIf the parentheses have no name, then their
contents is available in the match array by its number. Named
parentheses are also available in the property
undefinedundefinedgroups.undefinedundefined
We can also use parentheses contents in the
replacement string in undefinedundefinedstr.replace:
by the number undefinedundefined$n or the name
undefinedundefined$<name>.undefinedundefined
A group may be excluded from numbering by
adding undefinedundefinedpattern:?: in its start.
That's used when we need to apply a quantifier to the whole group,
but don't want it as a separate item in the results array. We also
can't reference such parentheses in the replacement
string.undefinedundefined
We can use the contents of
capturing groups undefinedundefinedpattern:(...) not
only in the result or in the replacement string, but also in the
pattern itself.undefinedundefined
A group can be referenced in the pattern using
undefinedundefinedpattern:\N, where
undefinedundefinedN is the group
number.undefinedundefined
To make clear why that's helpful, let's consider a task.
undefinedundefinedWe need to find quoted strings: either single-quoted
undefinedundefinedsubject:'...' or a double-quoted
undefinedundefinedsubject:"..." - both variants
should match.undefinedundefined
How to find them?
undefinedundefinedWe can put both kinds of quotes
in the square brackets:
undefinedundefinedpattern:['"](.*?)['"], but it would
find strings with mixed quotes, like
undefinedundefinedmatch:"...' and
undefinedundefinedmatch:'...". That would lead to
incorrect matches when one quote appears inside other ones, like
in the string
undefinedundefinedsubject:"She's the one!":undefinedundefined
``undefinedundefinedjs run let str =He said: "She's
the one!".`;undefinedundefined
let regexp = /undefinedundefined'''"['''"]/g;undefinedundefined
undefinedundefined// The result is not what we'd like to have alert( str.match(regexp) ); // "She'
undefinedundefinedAs we can see, the pattern found an opening
quote undefinedundefinedmatch:", then the text is
consumed till the other quote
undefinedundefinedmatch:', that closes the
match.undefinedundefined
To make sure that
the pattern looks for the closing quote exactly the same as the
opening one, we can wrap it into a capturing group and
backreference it:
undefinedundefinedpattern:(['"])(.*?)\1.undefinedundefined
Here's the correct code:
undefinedundefined
``undefinedundefinedjs run let str =He said: "She's
the one!".`;undefinedundefined
undefinedundefined! let regexp = /(['''"])(.undefinedundefined?)\1/g; /!*undefinedundefined
undefinedundefinedalert( str.match(regexp) ); // "She's the one!"
undefinedundefinedNow it works! The regular expression engine
finds the first quote
undefinedundefinedpattern:(['"]) and memorizes its
content. That's the first capturing group.undefinedundefined
Further in the pattern
undefinedundefinedpattern:\1 means "find the same
text as in the first group", exactly the same quote in our
case.undefinedundefined
Similar to that,
undefinedundefinedpattern:\2 would mean the contents
of the second group, undefinedundefinedpattern:\3 -
the 3rd group, and so on.undefinedundefined
undefinedundefinedIf we use `?:` in the group, then we can't reference it. Groups that are excluded from capturing `(?:...)` are not memorized by the engine.undefinedundefined
undefinedundefined
undefinedundefinedwarn header="Don't mess up: in the pattern `pattern:\1`, in the replacement: `pattern:$1`" In the replacement string we use a dollar sign: `pattern:$1`, while in the pattern - a backslash `pattern:\1`.undefinedundefined
\k<name>undefinedundefined
If a regexp has many parentheses, it's convenient to give them names.
undefinedundefinedTo
reference a named group we can use
undefinedundefinedpattern:\k<name>.undefinedundefined
In the example below the group with quotes
is named undefinedundefinedpattern:?<quote>, so
the backreference is
undefinedundefinedpattern:\k<quote>:undefinedundefined
``undefinedundefinedjs run let str =He said: "She's
the one!".`;undefinedundefined
undefinedundefined! let regexp = /(?undefinedundefined
['''"])(.undefinedundefined?)quote>/g;
/!*undefinedundefined
alert( str.match(regexp) ); // "She's the one!"
undefinedundefinedAlternation is the term in regular expression that is actually a simple "OR".
undefinedundefinedIn a regular expression it is denoted with a vertical line
character
undefinedundefinedpattern:|.undefinedundefined
For instance, we need to find programming languages: HTML, PHP, Java or JavaScript.
undefinedundefined
The corresponding regexp:
undefinedundefinedpattern:html|php|java(script)?.undefinedundefined
A usage example:
undefinedundefinedrun let regexp = /html|php|css|java(script)?/gi;
undefinedundefinedlet str = "First HTML appeared, then CSS, then JavaScript";
undefinedundefinedalert( str.match(regexp) ); // ‘HTML', ‘CSS', ‘JavaScript'
undefinedundefinedundefinedundefined
We already saw a similar thing -- square brackets. They allow to choose between multiple characters, for instance `pattern:gr[ae]y` matches `match:gray` or `match:grey`.
Square brackets allow only characters or character classes. Alternation allows any expressions. A regexp `pattern:A|B|C` means one of expressions `A`, `B` or `C`.
For instance:
- `pattern:gr(a|e)y` means exactly the same as `pattern:gr[ae]y`.
- `pattern:gra|ey` means `match:gra` or `match:ey`.
To apply alternation to a chosen part of the pattern, we can enclose it in parentheses:
- `pattern:I love HTML|CSS` matches `match:I love HTML` or `match:CSS`.
- `pattern:I love (HTML|CSS)` matches `match:I love HTML` or `match:I love CSS`.
## Example: regexp for time
In previous articles there was a task to build a regexp for searching time in the form `hh:mm`, for instance `12:00`. But a simple `pattern:\d\d:\d\d` is too vague. It accepts `25:99` as the time (as 99 minutes match the pattern, but that time is invalid).
How can we make a better pattern?
We can use more careful matching. First, the hours:
- If the first digit is `0` or `1`, then the next digit can be any: `pattern:[01]\d`.
- Otherwise, if the first digit is `2`, then the next must be `pattern:[0-3]`.
- (no other first digit is allowed)
We can write both variants in a regexp using alternation: `pattern:[01]\d|2[0-3]`.
Next, minutes must be from `00` to `59`. In the regular expression language that can be written as `pattern:[0-5]\d`: the first digit `0-5`, and then any digit.
If we glue hours and minutes together, we get the pattern: `pattern:[01]\d|2[0-3]:[0-5]\d`.
We're almost done, but there's a problem. The alternation `pattern:|` now happens to be between `pattern:[01]\d` and `pattern:2[0-3]:[0-5]\d`.
That is: minutes are added to the second alternation variant, here's a clear picture:
undefinedundefinedundefinedundefined[01] 2[0-3]:[0-5]``
undefinedundefinedThat pattern looks for
undefinedundefinedpattern:[01]\d or
undefinedundefinedpattern:2[0-3]:[0-5]\d.undefinedundefined
But that's wrong, the alternation should
only be used in the "hours" part of the regular expression, to
allow undefinedundefinedpattern:[01]\d OR
undefinedundefinedpattern:2[0-3]. Let's correct that
by enclosing "hours" into parentheses:
undefinedundefinedpattern:([01]\d|2[0-3]):[0-5]\d.undefinedundefined
The final solution:
undefinedundefinedrun let regexp = /([01]2[0-3]):[0-5]g;
undefinedundefinedalert("00:00 10:10 23:59 25:99 1:2".match(regexp)); // 00:00,10:10,23:59
undefinedundefinedWe often need to repeat actions.
undefinedundefinedFor example, outputting goods from a list one after another or just running the same code for each number from 1 to 10.
undefinedundefinedundefinedundefinedLoops are a way to repeat the same code multiple times.undefinedundefined
undefinedundefinedThe
undefinedundefinedwhile loop has the following
syntax:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedwhile (condition) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// codeundefinedundefinedundefinedundefinedundefinedundefined// so-called "loop body"undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
While the
undefinedundefinedcondition is truthy, the
undefinedundefinedcode from the loop body is
executed.undefinedundefined
For instance,
the loop below outputs undefinedundefinedi while
undefinedundefinedi < 3:undefinedundefined
undefinedundefinedjs run let i = 0; while (i < 3) { // shows 0, then 1, then 2 alert( i ); i++; }undefinedundefined
A single execution of the loop body is called undefinedundefinedan iteration. The loop in the example above makes three iterations.undefinedundefined
undefinedundefinedIf undefinedundefinedi++ was
missing from the example above, the loop would repeat (in theory)
forever. In practice, the browser provides ways to stop such
loops, and in server-side JavaScript, we can kill the
process.undefinedundefined
Any expression
or variable can be a loop condition, not just comparisons: the
condition is evaluated and converted to a boolean by
undefinedundefinedwhile.undefinedundefined
For instance, a shorter way to write
undefinedundefinedwhile (i != 0) is
undefinedundefinedwhile (i):undefinedundefined
undefinedundefinedjs run let i = 3; *!* while (i) { // when i becomes 0, the condition becomes falsy, and the loop stops */!* alert( i ); i--; }undefinedundefined
undefinedundefinedsmart header="Curly braces are not required for a single-line body" If the loop body has a single statement, we can omit the curly braces{…}`:undefinedundefined
undefinedundefinedjs run let i = 3; *!* while (i) alert(i--); */!*
undefinedundefined
The condition check can be moved
undefinedundefinedbelow the loop body using the
undefinedundefineddo..while syntax:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefineddoundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// loop bodyundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedwhile (condition)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The loop will first execute the body, then check the condition, and, while it's truthy, execute it again and again.
undefinedundefinedFor example:
undefinedundefined
undefinedundefinedjs run let i = 0; do { alert( i ); i++; } while (i < 3);undefinedundefined
This form of syntax should only be used
when you want the body of the loop to execute
undefinedundefinedat least once regardless of the
condition being truthy. Usually, the other form is preferred:
undefinedundefinedwhile(…) {…}.undefinedundefined
The undefinedundefinedfor loop is
more complex, but it's also the most commonly used
loop.undefinedundefined
It looks like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfor (beginundefinedundefined; conditionundefinedundefined; step) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ... loop body ...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Let's learn the meaning of these parts by
example. The loop below runs
undefinedundefinedalert(i) for
undefinedundefinedi from
undefinedundefined0 up to (but not including)
undefinedundefined3:undefinedundefined
undefinedundefinedjs run for (let i = 0; i < 3; i++) { // shows 0, then 1, then 2 alert(i); }undefinedundefined
Let's examine the
undefinedundefinedfor statement
part-by-part:undefinedundefined
| part | undefinedundefinedundefinedundefined | undefinedundefined |
|---|---|---|
| begin | undefinedundefined
undefinedundefinedi = 0undefinedundefined |
undefinedundefinedExecutes once upon entering the loop. | undefinedundefined
| condition | undefinedundefined
undefinedundefinedi < 3undefinedundefined
| undefinedundefinedChecked before every loop iteration. If false, the loop stops. | undefinedundefined
| body | undefinedundefined
undefinedundefinedalert(i)undefinedundefined
| undefinedundefinedRuns again and again while the condition is truthy. | undefinedundefined
| step | undefinedundefined
undefinedundefinedi++undefinedundefined |
undefinedundefinedExecutes after the body on each iteration. | undefinedundefined
The general loop algorithm works like this:
undefinedundefinedundefinedundefinedRun begin
→ (if condition → run body and run step)
→ (if condition → run body and run step)
→ (if condition → run body and run step)
→ ...undefinedundefinedundefinedundefinedThat is, undefinedundefinedbegin executes once,
and then it iterates: after each
undefinedundefinedcondition test,
undefinedundefinedbody and
undefinedundefinedstep are
executed.undefinedundefined
If you are new to loops, it could help to go back to the example and reproduce how it runs step-by-step on a piece of paper.
undefinedundefinedHere's exactly what happens in our case:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// for (let i = 0; i < 3; i++) alert(i)undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// run beginundefinedundefinedundefinedundefinedundefinedundefinedlet i undefinedundefined=undefinedundefined0undefinedundefinedundefinedundefinedundefinedundefined// if condition → run body and run stepundefinedundefinedundefinedundefinedundefinedundefinedif (i undefinedundefined<undefinedundefined3) undefinedundefined{undefinedundefinedalert(i)undefinedundefined; iundefinedundefined++undefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined// if condition → run body and run stepundefinedundefinedundefinedundefinedundefinedundefinedif (i undefinedundefined<undefinedundefined3) undefinedundefined{undefinedundefinedalert(i)undefinedundefined; iundefinedundefined++undefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined// if condition → run body and run stepundefinedundefinedundefinedundefinedundefinedundefinedif (i undefinedundefined<undefinedundefined3) undefinedundefined{undefinedundefinedalert(i)undefinedundefined; iundefinedundefined++undefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined// ...finish, because now i == 3undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedsmart header="Inline variable declaration" Here, the "counter" variablei`
is declared right in the loop. This is called an "inline" variable
declaration. Such variables are visible only inside the
loop.undefinedundefined
undefinedundefinedjs run for (*!*let*/!* i = 0; i < 3; i++) { alert(i); // 0, 1, 2 } alert(i); // error, no such variableundefinedundefined
Instead of defining a variable, we could use an existing one:
undefinedundefinedrun let i = 0;
undefinedundefinedfor (i = 0; i < 3; i++) { // use an existing variable alert(i); // 0, 1, 2 }
undefinedundefinedalert(i); // 3, visible, because declared outside of the loop
undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefinedAny part of undefinedundefinedfor
can be skipped.undefinedundefined
For
example, we can omit undefinedundefinedbegin if we
don't need to do anything at the loop start.undefinedundefined
Like here:
undefinedundefinedrun let i = 0; // we have i already declared and assigned
undefinedundefinedfor (; i < 3; i++) { // no need for "begin" alert( i ); // 0, 1, 2 }
undefinedundefinedWe can also remove the
undefinedundefinedstep part:undefinedundefined
run let i = 0;
undefinedundefinedfor (; i < 3;) { alert( i++ ); }
undefinedundefinedThis makes the loop identical to
undefinedundefinedwhile (i < 3).undefinedundefined
We can actually remove everything, creating an infinite loop:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefined;;) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// repeats without limitsundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Please note that the two
undefinedundefinedfor semicolons
undefinedundefined; must be present. Otherwise, there
would be a syntax error.undefinedundefined
Normally, a loop exits when its condition becomes falsy.
undefinedundefinedBut we can force the exit at any time using the
special undefinedundefinedbreak
directive.undefinedundefined
For example, the loop below asks the user for a series of numbers, "breaking" when no number is entered:
undefinedundefinedrun let sum = 0;
undefinedundefinedwhile (true) {
undefinedundefinedlet value = +prompt("Enter a number", '');
undefinedundefinedundefinedundefined! if (!value) break; // (undefinedundefined) /!*undefinedundefined
undefinedundefinedsum += value;
undefinedundefined} alert( ‘Sum:''' + sum );
undefinedundefinedThe undefinedundefinedbreak
directive is activated at the line
undefinedundefined(*) if the user enters an empty
line or cancels the input. It stops the loop immediately, passing
control to the first line after the loop. Namely,
undefinedundefinedalert.undefinedundefined
The combination "infinite loop +
undefinedundefinedbreak as needed" is great for
situations when a loop's condition must be checked not in the
beginning or end of the loop, but in the middle or even in several
places of its body.undefinedundefined
The
undefinedundefinedcontinue directive is a "lighter
version" of undefinedundefinedbreak. It doesn't stop
the whole loop. Instead, it stops the current iteration and forces
the loop to start a new one (if the condition
allows).undefinedundefined
We can use it if we're done with the current iteration and would like to move on to the next one.
undefinedundefinedThe loop below uses
undefinedundefinedcontinue to output only odd
values:undefinedundefined
run no-beautify for (let i = 0; i < 10; i++) {
undefinedundefined// if true, skip the remaining part of the body undefinedundefined!if (i % 2 == 0) continue;undefinedundefined/!undefinedundefined
undefinedundefinedalert(i); // 1, then 3, 5, 7, 9 }
undefinedundefinedFor even values of
undefinedundefinedi, the
undefinedundefinedcontinue directive stops executing
the body and passes control to the next iteration of
undefinedundefinedfor (with the next number). So the
undefinedundefinedalert is only called for odd
values.undefinedundefined
undefinedundefinedsmart header="Thecontinue`
directive helps decrease nesting" A loop that shows odd values
could look like this:undefinedundefined
run for (let i = 0; i < 10; i++) {
undefinedundefinedif (i % 2) { alert( i ); }
undefinedundefined}
undefinedundefinedundefinedundefined
From a technical point of view, this is identical to the example above. Surely, we can just wrap the code in an `if` block instead of using `continue`.
But as a side-effect, this created one more level of nesting (the `alert` call inside the curly braces). If the code inside of `if` is longer than a few lines, that may decrease the overall readability.undefinedundefined
undefinedundefined
undefinedundefinedwarn header="Nobreak/continueundefinedundefinedto the right side of '?'" Please note that syntax constructs that are not expressions cannot be used with the ternary operator?undefinedundefined. In particular, directives such asbreak/continue`
aren't allowed there.undefinedundefined
For example, if we take this code:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedif (i undefinedundefined>undefinedundefined5) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(i)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedelseundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedcontinueundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…and rewrite it using a question mark:
undefinedundefined
undefinedundefinedjs no-beautify (i > 5) ? alert(i) : *!*continue*/!*; // continue isn't allowed hereundefinedundefined
…it stops working: there's a syntax error.
undefinedundefinedThis is just another reason not to use the
question mark operator undefinedundefined? instead of
undefinedundefinedif.
undefinedundefined
Sometimes we need to break out from multiple nested loops at once.
undefinedundefinedFor example, in the
code below we loop over undefinedundefinedi and
undefinedundefinedj, prompting for the coordinates
undefinedundefined(i, j) from
undefinedundefined(0,0) to
undefinedundefined(2,2):undefinedundefined
run no-beautify for (let i = 0; i < 3; i++) {
undefinedundefinedfor (let j = 0; j < 3; j++) {
undefinedundefinedundefinedundefinedlet input = prompt(`Value at coords (${i},${j})`, '');
// what if we want to exit from here to Done (below)?undefinedundefinedundefinedundefined} }
undefinedundefinedalert(‘Done!''');
undefinedundefinedWe need a way to stop the process if the user cancels the input.
undefinedundefinedThe ordinary
undefinedundefinedbreak after
undefinedundefinedinput would only break the inner
loop. That's not sufficient - labels, come to the
rescue!undefinedundefined
A undefinedundefinedlabel is an identifier with a colon before a loop:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlabelNameundefinedundefined:undefinedundefinedfor (...) undefinedundefined{undefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The
undefinedundefinedbreak <labelName> statement
in the loop below breaks out to the label:undefinedundefined
run no-beautify undefinedundefined!outer:undefinedundefined/! for (let i = 0; i < 3; i++) {undefinedundefined
undefinedundefinedfor (let j = 0; j < 3; j++) {
undefinedundefinedundefinedundefinedlet input = prompt(`Value at coords (${i},${j})`, '');
// if an empty string or canceled, then break out of both loops
if (!input) *!*break outer*/!*; // (*)
// do something with the value...undefinedundefinedundefinedundefined} } alert(‘Done!''');
undefinedundefinedIn the code above,
undefinedundefinedbreak outer looks upwards for the
label named undefinedundefinedouter and breaks out of
that loop.undefinedundefined
So the
control goes straight from undefinedundefined(*) to
undefinedundefinedalert('Done!').undefinedundefined
We can also move the label onto a separate line:
undefinedundefined
undefinedundefinedjs no-beautify outer: for (let i = 0; i < 3; i++) { ... }undefinedundefined
The undefinedundefinedcontinue
directive can also be used with a label. In this case, code
execution jumps to the next iteration of the labeled
loop.undefinedundefined
warn header="Labels do not allow to "jump" anywhere" Labels do not allow us to jump into an arbitrary place in the code.
undefinedundefinedFor example, it is impossible to do this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedbreak labelundefinedundefined;undefinedundefined// jump to the label below (doesn't work)undefinedundefinedundefinedundefinedundefinedundefinedlabelundefinedundefined:undefinedundefinedfor (...)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
A undefinedundefinedbreak
directive must be inside a code block. Technically, any labelled
code block will do, e.g.:undefinedundefined
undefinedundefinedundefinedundefinedlabelundefinedundefined:undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefinedbreak labelundefinedundefined;undefinedundefined// worksundefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…Although, 99.9% of the time
undefinedundefinedbreak used is inside loops, as
we've seen in the examples above.undefinedundefined
A undefinedundefinedcontinue is
only possible from inside a loop.
undefinedundefined
We covered 3 types of loops:
undefinedundefinedwhile - The condition is checked
before each iteration.undefinedundefineddo..while - The condition is
checked after each iteration.undefinedundefinedfor (;;) -
The condition is checked before each iteration, additional
settings available.undefinedundefinedTo make an "infinite" loop, usually the
undefinedundefinedwhile(true) construct is used. Such
a loop, just like any other, can be stopped with the
undefinedundefinedbreak directive.undefinedundefined
If we don't want to do anything in the
current iteration and would like to forward to the next one, we
can use the undefinedundefinedcontinue
directive.undefinedundefined
undefinedundefinedbreak/continue support labels
before the loop. A label is the only way for
undefinedundefinedbreak/continue to escape a nested
loop to go to an outer one.undefinedundefined
Sometimes we need to find only those matches for a pattern that are followed or preceded by another pattern.
undefinedundefinedThere's a special syntax for that, called "lookahead" and "lookbehind", together referred to as "lookaround".
undefinedundefinedFor the start, let's
find the price from the string like
undefinedundefinedsubject:1 turkey costs 30€. That
is: a number, followed by undefinedundefinedsubject:€
sign.undefinedundefined
The syntax is:
undefinedundefinedpattern:X(?=Y), it means "look for
undefinedundefinedpattern:X, but match only if
followed by undefinedundefinedpattern:Y". There may
be any pattern instead of undefinedundefinedpattern:X
and undefinedundefinedpattern:Y.undefinedundefined
For an integer number followed by
undefinedundefinedsubject:€, the regexp will be
undefinedundefinedpattern:\d+(?=€):undefinedundefined
run let str = "1 turkey costs 30€";
undefinedundefinedalert( str.match(/(?=€)/) ); // 30, the number 1 is ignored, as it's not followed by €
undefinedundefinedPlease note: the lookahead is merely a
test, the contents of the parentheses
undefinedundefinedpattern:(?=...) is not included in
the result
undefinedundefinedmatch:30.undefinedundefined
When we look for
undefinedundefinedpattern:X(?=Y), the regular
expression engine finds undefinedundefinedpattern:X
and then checks if there's
undefinedundefinedpattern:Y immediately after it. If
it's not so, then the potential match is skipped, and the search
continues.undefinedundefined
More complex
tests are possible,
e.g. undefinedundefinedpattern:X(?=Y)(?=Z)
means:undefinedundefined
pattern:X.undefinedundefinedpattern:Y is immediately after
undefinedundefinedpattern:X (skip if
isn't).undefinedundefinedpattern:Z is also immediately
after undefinedundefinedpattern:X (skip if
isn't).undefinedundefinedpattern:X
is a match, otherwise continue searching.undefinedundefinedIn other words, such
pattern means that we're looking for
undefinedundefinedpattern:X followed by
undefinedundefinedpattern:Y and
undefinedundefinedpattern:Z at the same
time.undefinedundefined
That's only
possible if patterns undefinedundefinedpattern:Y and
undefinedundefinedpattern:Z aren't mutually
exclusive.undefinedundefined
For example,
undefinedundefinedpattern:\d+(?=\s)(?=.*30) looks for
undefinedundefinedpattern:\d+ that is followed by a
space undefinedundefinedpattern:(?=\s), and there's
undefinedundefined30 somewhere after it
undefinedundefinedpattern:(?=.*30):undefinedundefined
run let str = "1 turkey costs 30€";
undefinedundefinedalert( str.match(/(?=)(?=.*30)/) ); // 1
undefinedundefinedIn our string that exactly matches the
number undefinedundefined1.undefinedundefined
Let's say that we want a quantity instead,
not a price from the same string. That's a number
undefinedundefinedpattern:\d+, NOT followed by
undefinedundefinedsubject:€.undefinedundefined
For that, a negative lookahead can be applied.
undefinedundefinedThe syntax is:
undefinedundefinedpattern:X(?!Y), it means "search
undefinedundefinedpattern:X, but only if not followed
by undefinedundefinedpattern:Y".undefinedundefined
run let str = "2 turkeys cost 60€";
undefinedundefinedalert( str.match(/?!€)/g) ); // 2 (the price is not matched)
undefinedundefinedLookahead allows to add a condition for "what follows".
undefinedundefinedLookbehind is similar, but it looks behind. That is, it allows to match a pattern only if there's something before it.
undefinedundefinedThe syntax
is: - Positive lookbehind:
undefinedundefinedpattern:(?<=Y)X, matches
undefinedundefinedpattern:X, but only if there's
undefinedundefinedpattern:Y before it. - Negative
lookbehind: undefinedundefinedpattern:(?<!Y)X,
matches undefinedundefinedpattern:X, but only if
there's no undefinedundefinedpattern:Y before
it.undefinedundefined
For example, let's
change the price to US dollars. The dollar sign is usually before
the number, so to look for undefinedundefined$30
we'll use undefinedundefinedpattern:(?<=\$)\d+ -
an amount preceded by
undefinedundefinedsubject:$:undefinedundefined
run let str = "1 turkey costs $30";
undefinedundefined// the dollar sign is escaped $ alert( str.match(/(?<=$)/) ); // 30 (skipped the sole number)
undefinedundefinedAnd, if we need the quantity - a number,
not preceded by undefinedundefinedsubject:$, then we
can use a negative lookbehind
undefinedundefinedpattern:(?<!\$)\d+:undefinedundefined
run let str = "2 turkeys cost $60";
undefinedundefinedalert( str.match(/(?<!$)/g) ); // 2 (the price is not matched)
undefinedundefinedGenerally, the contents inside lookaround parentheses does not become a part of the result.
undefinedundefinedE.g. in the pattern
undefinedundefinedpattern:\d+(?=€), the
undefinedundefinedpattern:€ sign doesn't get captured
as a part of the match. That's natural: we look for a number
undefinedundefinedpattern:\d+, while
undefinedundefinedpattern:(?=€) is just a test that
it should be followed by
undefinedundefinedsubject:€.undefinedundefined
But in some situations we might want to capture the lookaround expression as well, or a part of it. That's possible. Just wrap that part into additional parentheses.
undefinedundefinedIn the example below the currency sign
undefinedundefinedpattern:(€|kr) is captured, along
with the amount:undefinedundefined
run let str = "1 turkey costs 30€"; let regexp = /(?=(€|kr))/; // extra parentheses around €|kr
undefinedundefinedalert( str.match(regexp) ); // 30, €
undefinedundefinedAnd here's the same for lookbehind:
undefinedundefinedrun let str = "1 turkey costs $30"; let regexp = /(?<=($|£))/;
undefinedundefinedalert( str.match(regexp) ); // 30, $
undefinedundefinedLookahead and lookbehind (commonly referred to as "lookaround") are useful when we'd like to match something depending on the context before/after it.
undefinedundefinedFor simple regexps we can do the similar thing manually. That is: match everything, in any context, and then filter by context in the loop.
undefinedundefinedRemember,
undefinedundefinedstr.match (without flag
undefinedundefinedpattern:g) and
undefinedundefinedstr.matchAll (always) return
matches as arrays with undefinedundefinedindex
property, so we know where exactly in the text it is, and can
check the context.undefinedundefined
But generally lookaround is more convenient.
undefinedundefinedLookaround types:
undefinedundefined| Pattern | undefinedundefinedtype | undefinedundefinedmatches | undefinedundefined
|---|---|---|
undefinedundefinedX(?=Y)undefinedundefined |
undefinedundefinedPositive lookahead | undefinedundefinedundefinedundefinedpattern:X
if followed by
undefinedundefinedpattern:Yundefinedundefined
| undefinedundefined
undefinedundefinedX(?!Y)undefinedundefined |
undefinedundefinedNegative lookahead | undefinedundefinedundefinedundefinedpattern:X
if not followed by
undefinedundefinedpattern:Yundefinedundefined
| undefinedundefined
undefinedundefined(?<=Y)Xundefinedundefined
| undefinedundefinedPositive lookbehind | undefinedundefinedundefinedundefinedpattern:X
if after
undefinedundefinedpattern:Yundefinedundefined
| undefinedundefined
undefinedundefined(?<!Y)Xundefinedundefined
| undefinedundefinedNegative lookbehind | undefinedundefinedundefinedundefinedpattern:X
if not after
undefinedundefinedpattern:Yundefinedundefined
| undefinedundefined
Some regular expressions are looking simple, but can execute a veeeeeery long time, and even "hang" the JavaScript engine.
undefinedundefinedSooner or later most developers occasionally face such behavior. The typical symptom - a regular expression works fine sometimes, but for certain strings it "hangs", consuming 100% of CPU.
undefinedundefinedIn such case a web-browser suggests to kill the script and reload the page. Not a good thing for sure.
undefinedundefinedFor server-side JavaScript such a regexp may hang the server process, that's even worse. So we definitely should take a look at it.
undefinedundefined
Let's say we have a string, and we'd like to check if it consists
of words undefinedundefinedpattern:\w+ with an
optional space undefinedundefinedpattern:\s? after
each.undefinedundefined
An obvious way to
construct a regexp would be to take a word followed by an optional
space undefinedundefinedpattern:\w+\s? and then
repeat it with undefinedundefined*.undefinedundefined
That leads us to the regexp
undefinedundefinedpattern:^(\w+\s?)*$, it specifies
zero or more such words, that start at the beginning
undefinedundefinedpattern:^ and finish at the end
undefinedundefinedpattern:$ of the
line.undefinedundefined
In action:
undefinedundefinedrun let regexp = /^(+?)*$/;
undefinedundefinedalert( regexp.test("A good string") ); // true alert( regexp.test("Bad characters: $@#") ); // false
undefinedundefinedThe regexp seems to work. The result is correct. Although, on certain strings it takes a lot of time. So long that JavaScript engine "hangs" with 100% CPU consumption.
undefinedundefinedIf you run the example below, you probably won't see anything, as JavaScript will just "hang". A web-browser will stop reacting on events, the UI will stop working (most browsers allow only scrolling). After some time it will suggest to reload the page. So be careful with this:
undefinedundefinedrun let regexp = /^(+?)*$/; let str = "An input string that takes a long time or even makes this regexp hang!";
undefinedundefined// will take a very long time alert( regexp.test(str) );
undefinedundefinedTo be fair, let's note that some regular expression engines can handle such a search effectively, for example V8 engine version starting from 8.8 can do that (so Google Chrome 88 doesn't hang here), while Firefox browser does hang.
undefinedundefinedWhat's the matter? Why does the regular expression hang?
undefinedundefinedTo understand that, let's
simplify the example: remove spaces
undefinedundefinedpattern:\s?. Then it becomes
undefinedundefinedpattern:^(\w+)*$.undefinedundefined
And, to make things more obvious, let's
replace undefinedundefinedpattern:\w with
undefinedundefinedpattern:\d. The resulting regular
expression still hangs, for instance:undefinedundefined
run let regexp = /^()*$/;
undefinedundefinedlet str = "012345678901234567890123456789z";
undefinedundefined// will take a very long time (careful!) alert( regexp.test(str) );
undefinedundefinedundefinedundefined
So what's wrong with the regexp?
First, one may notice that the regexp `pattern:(\d+)*` is a little bit strange. The quantifier `pattern:*` looks extraneous. If we want a number, we can use `pattern:\d+`.
Indeed, the regexp is artificial; we got it by simplifying the previous example. But the reason why it is slow is the same. So let's understand it, and then the previous example will become obvious.
What happens during the search of `pattern:^(\d+)*$` in the line `subject:123456789z` (shortened a bit for clarity, please note a non-digit character `subject:z` at the end, it's important), why does it take so long?
Here's what the regexp engine does:
1. First, the regexp engine tries to find the content of the parentheses: the number `pattern:\d+`. The plus `pattern:+` is greedy by default, so it consumes all digits:
\d+.......
(123456789)z
After all digits are consumed, `pattern:\d+` is considered found (as `match:123456789`).
Then the star quantifier `pattern:(\d+)*` applies. But there are no more digits in the text, so the star doesn't give anything.
The next character in the pattern is the string end `pattern:$`. But in the text we have `subject:z` instead, so there's no match:
X
\d+........$
(123456789)z
2. As there's no match, the greedy quantifier `pattern:+` decreases the count of repetitions, backtracks one character back.
Now `pattern:\d+` takes all digits except the last one (`match:12345678`):
\d+.......
(12345678)9z
3. Then the engine tries to continue the search from the next position (right after `match:12345678`).
The star `pattern:(\d+)*` can be applied -- it gives one more match of `pattern:\d+`, the number `match:9`:
\d+.......\d+
(12345678)(9)z
The engine tries to match `pattern:$` again, but fails, because it meets `subject:z` instead:
X
\d+.......\d+
(12345678)(9)z
4. There's no match, so the engine will continue backtracking, decreasing the number of repetitions. Backtracking generally works like this: the last greedy quantifier decreases the number of repetitions until it reaches the minimum. Then the previous greedy quantifier decreases, and so on.
All possible combinations are attempted. Here are their examples.
The first number `pattern:\d+` has 7 digits, and then a number of 2 digits:
X
\d+......\d+
(1234567)(89)z
The first number has 7 digits, and then two numbers of 1 digit each:
X
\d+......\d+\d+
(1234567)(8)(9)z
The first number has 6 digits, and then a number of 3 digits:
X
\d+.......\d+
(123456)(789)z
The first number has 6 digits, and then 2 numbers:
X
\d+.....\d+ \d+
(123456)(78)(9)z
...And so on.
There are many ways to split a sequence of digits `123456789` into numbers. To be precise, there are <code>2<sup>n</sup>-1</code>, where `n` is the length of the sequence.
- For `123456789` we have `n=9`, that gives 511 combinations.
- For a longer sequence with `n=20` there are about one million (1048575) combinations.
- For `n=30` - a thousand times more (1073741823 combinations).
Trying each of them is exactly the reason why the search takes so long.
## Back to words and strings
The similar thing happens in our first example, when we look for words by pattern `pattern:^(\w+\s?)*$` in the string `subject:An input that hangs!`.
The reason is that a word can be represented as one `pattern:\w+` or many:
undefinedundefinedundefinedundefined(input) (inpu)(t) (inp)(u)(t) (in)(p)(ut) …
undefinedundefinedFor a human, it's obvious that there may be
no match, because the string ends with an exclamation sign
undefinedundefined!, but the regular expression
expects a wordly character
undefinedundefinedpattern:\w or a space
undefinedundefinedpattern:\s at the end. But the
engine doesn't know that.undefinedundefined
It tries all combinations of how the regexp
undefinedundefinedpattern:(\w+\s?)* can "consume" the
string, including variants with spaces
undefinedundefinedpattern:(\w+\s)* and without them
undefinedundefinedpattern:(\w+)* (because spaces
undefinedundefinedpattern:\s? are optional). As there
are many such combinations (we've seen it with digits), the search
takes a lot of time.undefinedundefined
What to do?
undefinedundefinedShould we turn on the lazy mode?
undefinedundefinedUnfortunately, that won't help: if
we replace undefinedundefinedpattern:\w+ with
undefinedundefinedpattern:\w+?, the regexp will still
hang. The order of combinations will change, but not their total
count.undefinedundefined
Some regular expression engines have tricky tests and finite automations that allow to avoid going through all combinations or make it much faster, but most engines don't, and it doesn't always help.
undefinedundefinedThere are two main approaches to fixing the problem.
undefinedundefinedThe first is to lower the number of possible combinations.
undefinedundefinedLet's make the
space non-optional by rewriting the regular expression as
undefinedundefinedpattern:^(\w+\s)*\w*$ - we'll look
for any number of words followed by a space
undefinedundefinedpattern:(\w+\s)*, and then
(optionally) a final word
undefinedundefinedpattern:\w*.undefinedundefined
This regexp is equivalent to the previous one (matches the same) and works well:
undefinedundefinedrun let regexp = /^(+)*$/; let str = "An input string that takes a long time or even makes this regex hang!";
undefinedundefinedalert( regexp.test(str) ); // false
undefinedundefinedundefinedundefined
Why did the problem disappear?
That's because now the space is mandatory.
The previous regexp, if we omit the space, becomes `pattern:(\w+)*`, leading to many combinations of `\w+` within a single word
So `subject:input` could be matched as two repetitions of `pattern:\w+`, like this:
undefinedundefinedundefinedundefined+ + (inp)(ut)
undefinedundefinedundefinedundefined
The new pattern is different: `pattern:(\w+\s)*` specifies repetitions of words followed by a space! The `subject:input` string can't be matched as two repetitions of `pattern:\w+\s`, because the space is mandatory.
The time needed to try a lot of (actually most of) combinations is now saved.
## Preventing backtracking
It's not always convenient to rewrite a regexp though. In the example above it was easy, but it's not always obvious how to do it.
Besides, a rewritten regexp is usually more complex, and that's not good. Regexps are complex enough without extra efforts.
Luckily, there's an alternative approach. We can forbid backtracking for the quantifier.
The root of the problem is that the regexp engine tries many combinations that are obviously wrong for a human.
E.g. in the regexp `pattern:(\d+)*$` it's obvious for a human, that `pattern:+` shouldn't backtrack. If we replace one `pattern:\d+` with two separate `pattern:\d+\d+`, nothing changes:
undefinedundefinedundefinedundefined…….. (123456789)!
undefinedundefined……. (1234)(56789)!
undefinedundefinedAnd in the original example
undefinedundefinedpattern:^(\w+\s?)*$ we may want to
forbid backtracking in undefinedundefinedpattern:\w+.
That is: undefinedundefinedpattern:\w+ should match a
whole word, with the maximal possible length. There's no need to
lower the repetitions count in
undefinedundefinedpattern:\w+ or to split it into two
words undefinedundefinedpattern:\w+\w+ and so
on.undefinedundefined
Modern regular
expression engines support possessive quantifiers for that.
Regular quantifiers become possessive if we add
undefinedundefinedpattern:+ after them. That is, we
use undefinedundefinedpattern:\d++ instead of
undefinedundefinedpattern:\d+ to stop
undefinedundefinedpattern:+ from
backtracking.undefinedundefined
Possessive quantifiers are in fact simpler than "regular" ones. They just match as many as they can, without any backtracking. The search process without bracktracking is simpler.
undefinedundefinedThere are also so-called "atomic capturing groups" - a way to disable backtracking inside parentheses.
undefinedundefined…But the bad news is that, unfortunately, in JavaScript they are not supported.
undefinedundefinedWe can emulate them though using a "lookahead transform".
undefinedundefinedSo we've come to real advanced topics. We'd
like a quantifier, such as
undefinedundefinedpattern:+ not to backtrack, because
sometimes backtracking makes no sense.undefinedundefined
The pattern to take as many repetitions of
undefinedundefinedpattern:\w as possible without
backtracking is:
undefinedundefinedpattern:(?=(\w+))\1. Of course, we
could take another pattern instead of
undefinedundefinedpattern:\w.undefinedundefined
That may seem odd, but it's actually a very simple transform.
undefinedundefinedLet's decipher it:
undefinedundefinedpattern:?= looks forward for the
longest word undefinedundefinedpattern:\w+ starting
at the current position.undefinedundefinedpattern:?=... isn't memorized by
the engine, so wrap undefinedundefinedpattern:\w+
into parentheses. Then the engine will memorize their
contentsundefinedundefinedpattern:\1.undefinedundefined
That is: we look
ahead - and if there's a word
undefinedundefinedpattern:\w+, then match it as
undefinedundefinedpattern:\1.undefinedundefined
Why? That's because the lookahead finds a word
undefinedundefinedpattern:\w+ as a whole and we
capture it into the pattern with
undefinedundefinedpattern:\1. So we essentially
implemented a possessive plus
undefinedundefinedpattern:+ quantifier. It captures
only the whole word undefinedundefinedpattern:\w+,
not a part of it.undefinedundefined
For
instance, in the word
undefinedundefinedsubject:JavaScript it may not only
match undefinedundefinedmatch:Java, but leave out
undefinedundefinedmatch:Script to match the rest of
the pattern.undefinedundefined
Here's the comparison of two patterns:
undefinedundefined
undefinedundefinedjs run alert( "JavaScript".match(/\w+Script/)); // JavaScript alert( "JavaScript".match(/(?=(\w+))\1Script/)); // nullundefinedundefined
pattern:\w+ first
captures the whole word
undefinedundefinedsubject:JavaScript but then
undefinedundefinedpattern:+ backtracks character by
character, to try to match the rest of the pattern, until it
finally succeeds (when
undefinedundefinedpattern:\w+ matches
undefinedundefinedmatch:Java).undefinedundefined
pattern:(?=(\w+)) looks ahead and
finds the word
undefinedundefinedsubject:JavaScript, that is
included into the pattern as a whole by
undefinedundefinedpattern:\1, so there remains no
way to find undefinedundefinedsubject:Script after
it.undefinedundefinedWe can put a more complex regular expression
into undefinedundefinedpattern:(?=(\w+))\1 instead of
undefinedundefinedpattern:\w, when we need to forbid
backtracking for undefinedundefinedpattern:+ after
it.undefinedundefined
undefinedundefinedThere's more about the relation between possessive quantifiers and lookahead in articles [Regex: Emulate Atomic Grouping (and Possessive Quantifiers) with LookAhead](http://instanceof.me/post/52245507631/regex-emulate-atomic-grouping-with-lookahead) and [Mimicking Atomic Groups](http://blog.stevenlevithan.com/archives/mimic-atomic-groups).undefinedundefined
undefinedundefinedLet's rewrite the first example using lookahead to prevent backtracking:
undefinedundefinedrun let regexp = /^((?=(+))\2?)*$/;
undefinedundefinedalert( regexp.test("A good string") ); // true
undefinedundefinedlet str = "An input string that takes a long time or even makes this regex hang!";
undefinedundefinedalert( regexp.test(str) ); // false, works and fast!
undefinedundefinedHere
undefinedundefinedpattern:\2 is used instead of
undefinedundefinedpattern:\1, because there are
additional outer parentheses. To avoid messing up with the
numbers, we can give the parentheses a name,
e.g. undefinedundefinedpattern:(?<word>\w+).undefinedundefined
run // parentheses are named
?undefinedundefined
let str = "An input string that takes a long time or even makes this regex hang!";
undefinedundefinedalert( regexp.test(str) ); // false
undefinedundefinedalert( regexp.test("A correct string") ); // true
undefinedundefinedThe problem described in this article is called "catastrophic backtracking".
undefinedundefinedWe covered two ways how to solve it: - Rewrite the regexp to lower the possible combinations count. - Prevent backtracking.
undefinedundefined
The flag undefinedundefinedpattern:y allows to
perform the search at the given position in the source
string.undefinedundefined
To grasp the use
case of undefinedundefinedpattern:y flag, and better
understand the ways of regexps, let's explore a practical
example.undefinedundefined
One of common tasks for regexps is "lexical analysis": we get a text, e.g. in a programming language, and need to find its structural elements. For instance, HTML has tags and attributes, JavaScript code has functions, variables, and so on.
undefinedundefinedWriting lexical analyzers is a special area, with its own tools and algorithms, so we don't go deep in there, but there's a common task: to read something at the given position.
undefinedundefinedE.g. we have a code string
undefinedundefinedsubject:let varName = "value", and
we need to read the variable name from it, that starts at position
undefinedundefined4.undefinedundefined
We'll look for variable name using regexp
undefinedundefinedpattern:\w+. Actually, JavaScript
variable names need a bit more complex regexp for accurate
matching, but here it doesn't matter.undefinedundefined
str.match(/\w+/) will find only
the first word in the line (undefinedundefinedlet).
That's not it.undefinedundefinedpattern:g. But
then the call undefinedundefinedstr.match(/\w+/g)
will look for all words in the text, while we need one word at
position undefinedundefined4. Again, not what we
need.undefinedundefinedundefinedundefinedSo, how to search for a regexp exactly at the given position?undefinedundefined
undefinedundefined
Let's try using method
undefinedundefinedregexp.exec(str).undefinedundefined
For a undefinedundefinedregexp
without flags undefinedundefinedpattern:g and
undefinedundefinedpattern:y, this method looks only
for the first match, it works exactly like
undefinedundefinedstr.match(regexp).undefinedundefined
…But if there's flag
undefinedundefinedpattern:g, then it performs the
search in undefinedundefinedstr, starting from
position stored in the
undefinedundefinedregexp.lastIndex property. And, if
it finds a match, then sets
undefinedundefinedregexp.lastIndex to the index
immediately after the match.undefinedundefined
In other words,
undefinedundefinedregexp.lastIndex serves as a
starting point for the search, that each
undefinedundefinedregexp.exec(str) call resets to the
new value ("after the last match"). That's only if there's
undefinedundefinedpattern:g flag, of
course.undefinedundefined
So, successive
calls to undefinedundefinedregexp.exec(str) return
matches one after another.undefinedundefined
Here's an example of such calls:
undefinedundefinedrun let str = ‘let varName'; // Let's find all words in this string let regexp = /+/g;
undefinedundefinedalert(regexp.lastIndex); // 0 (initially lastIndex=0)
undefinedundefinedlet word1 = regexp.exec(str); alert(word1[0]); // let (1st word) alert(regexp.lastIndex); // 3 (position after the match)
undefinedundefinedlet word2 = regexp.exec(str); alert(word2[0]); // varName (2nd word) alert(regexp.lastIndex); // 11 (position after the match)
undefinedundefinedlet word3 = regexp.exec(str); alert(word3); // null (no more matches) alert(regexp.lastIndex); // 0 (resets at search end)
undefinedundefinedWe can get all matches in the loop:
undefinedundefinedrun let str = ‘let varName'; let regexp = /+/g;
undefinedundefinedlet result;
undefinedundefined
while (result = regexp.exec(str)) { alert(
undefinedundefinedFound ${result[0]} at position ${result.index}
); // Found let at position 0, then // Found varName at position 4
}
undefinedundefined
Such use of
undefinedundefinedregexp.exec is an alternative to
method undefinedundefinedstr.matchAll, with a bit
more control over the process.undefinedundefined
Let's go back to our task.
undefinedundefinedWe can manually set
undefinedundefinedlastIndex to
undefinedundefined4, to start the search from the
given position!undefinedundefined
Like this:
undefinedundefinedrun let str = ‘let varName = "value"';
undefinedundefinedlet regexp = /+/g; // without flag "g", property lastIndex is ignored
undefinedundefinedundefinedundefined! regexp.lastIndex = 4; undefinedundefined/!undefinedundefined
undefinedundefinedlet word = regexp.exec(str); alert(word); // varName
undefinedundefinedHooray! Problem solved!
undefinedundefinedWe performed a search of
undefinedundefinedpattern:\w+, starting from position
undefinedundefinedregexp.lastIndex = 4.undefinedundefined
The result is correct.
undefinedundefined…But wait, not so fast.
undefinedundefinedPlease note: the undefinedundefinedregexp.exec call
starts searching at position
undefinedundefinedlastIndex and then goes further. If
there's no word at position
undefinedundefinedlastIndex, but it's somewhere after
it, then it will be found:undefinedundefined
run let str = ‘let varName = "value"';
undefinedundefinedlet regexp = /+/g;
undefinedundefinedundefinedundefined! // start the search from position 3 regexp.lastIndex = 3; undefinedundefined/!undefinedundefined
undefinedundefinedlet word = regexp.exec(str); // found the match at position 4 alert(word[0]); // varName alert(word.index); // 4
undefinedundefinedFor some tasks, including the lexical
analysis, that's just wrong. We need to find a match exactly at
the given position at the text, not somewhere after it. And that's
what the flag undefinedundefinedy is
for.undefinedundefined
undefinedundefinedThe flag
undefinedundefinedpattern:y makes
undefinedundefinedregexp.exec to search exactly at
position undefinedundefinedlastIndex, not "starting
from" it.undefinedundefinedundefinedundefined
Here's the same search with flag
undefinedundefinedpattern:y:undefinedundefined
run let str = ‘let varName = "value"';
undefinedundefinedlet regexp = /+/y;
undefinedundefinedregexp.lastIndex = 3; alert( regexp.exec(str) ); // null (there's a space at position 3, not a word)
undefinedundefinedregexp.lastIndex = 4; alert( regexp.exec(str) ); // varName (word at position 4)
undefinedundefinedAs we can see, regexp
undefinedundefinedpattern:/\w+/y doesn't match at
position undefinedundefined3 (unlike the flag
undefinedundefinedpattern:g), but matches at position
undefinedundefined4.undefinedundefined
Not only that's what we need, there's an
important performance gain when using flag
undefinedundefinedpattern:y.undefinedundefined
Imagine, we have a long text, and there are no
matches in it, at all. Then a search with flag
undefinedundefinedpattern:g will go till the end of
the text and find nothing, and this will take significantly more
time than the search with flag
undefinedundefinedpattern:y, that checks only the
exact position.undefinedundefined
In tasks
like lexical analysis, there are usually many searches at an exact
position, to check what we have there. Using flag
undefinedundefinedpattern:y is the key for correct
implementations and a good performance.undefinedundefined
In this article we'll cover various methods that work with regexps in-depth.
undefinedundefinedThe method
undefinedundefinedstr.match(regexp) finds matches for
undefinedundefinedregexp in the string
undefinedundefinedstr.undefinedundefined
It has 3 modes:
undefinedundefinedIf the
undefinedundefinedregexp doesn't have flag
undefinedundefinedpattern:g, then it returns the
first match as an array with capturing groups and properties
undefinedundefinedindex (position of the match),
undefinedundefinedinput (input string, equals
undefinedundefinedstr):undefinedundefined
run let str = "I love JavaScript";
undefinedundefinedlet result = str.match(/Java(Script)/);
undefinedundefinedalert( result[0] ); // JavaScript (full match) alert( result[1] ); // Script (first capturing group) alert( result.length ); // 2
undefinedundefined// Additional information: alert( result.index ); // 7 (match position) alert( result.input ); // I love JavaScript (source string)
undefinedundefinedIf the
undefinedundefinedregexp has flag
undefinedundefinedpattern:g, then it returns an
array of all matches as strings, without capturing groups and
other details. run let str = "I love
JavaScript";undefinedundefined
let result = str.match(/Java(Script)/g);
undefinedundefinedalert( result[0] ); // JavaScript alert( result.length ); // 1
undefinedundefinedIf there are no matches, no matter if
there's flag undefinedundefinedpattern:g or not,
undefinedundefinednull is
returned.undefinedundefined
That's an
important nuance. If there are no matches, we don't get an
empty array, but undefinedundefinednull. It's
easy to make a mistake forgetting about it,
e.g.:undefinedundefined
run let str = "I love JavaScript";
undefinedundefinedlet result = str.match(/HTML/);
undefinedundefinedalert(result); // null alert(result.length); // Error: Cannot read property ‘length' of null
undefinedundefinedIf we want the result to be an array, we can write like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet result undefinedundefined=undefinedundefinedstr.undefinedundefinedmatch(regexp) undefinedundefined|| []undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
[recent browser="new"]
undefinedundefinedThe method
undefinedundefinedstr.matchAll(regexp) is a "newer,
improved" variant of
undefinedundefinedstr.match.undefinedundefined
It's used mainly to search for all matches with all groups.
undefinedundefinedThere are 3 differences from
undefinedundefinedmatch:undefinedundefined
Array.from.undefinedundefined
str.match without flag
undefinedundefinedpattern:g).undefinedundefined
null, but an empty iterable
object.undefinedundefinedUsage example:
run let str = ''' undefinedundefined'''; let regexp = /<(.*?)>/g;
undefinedundefinedlet matchAll = str.matchAll(regexp);
undefinedundefinedalert(matchAll); // [object RegExp String Iterator], not array, but an iterable
undefinedundefinedmatchAll = Array.from(matchAll); // array now
let firstMatch = matchAll[0]; alert( firstMatch[0] ); // undefinedundefinedundefinedundefined
If we use
undefinedundefinedfor..of to loop over
undefinedundefinedmatchAll matches, then we don't
need undefinedundefinedArray.from any
more.undefinedundefined
Splits the string using the regexp (or a substring) as a delimiter.
undefinedundefinedWe can use
undefinedundefinedsplit with strings, like
this:undefinedundefined
undefinedundefinedjs run alert('12-34-56'.split('-')) // array of ['12', '34', '56']undefinedundefined
But we can split by a regular expression, the same way:
undefinedundefined
undefinedundefinedjs run alert('12, 34, 56'.split(/,\s*/)) // array of ['12', '34', '56']undefinedundefined
The method
undefinedundefinedstr.search(regexp) returns the
position of the first match or undefinedundefined-1
if none found:undefinedundefined
run let str = "A drop of ink may make a million think";
undefinedundefinedalert( str.search( /ink/i ) ); // 10 (first match position)
undefinedundefinedundefinedundefinedThe important
limitation: undefinedundefinedsearch only finds
the first match.undefinedundefinedundefinedundefined
If we need positions of further matches,
we should use other means, such as finding them all with
undefinedundefinedstr.matchAll(regexp).undefinedundefined
This is a generic method for searching and replacing, one of most useful ones. The swiss army knife for searching and replacing.
undefinedundefinedWe can use it without regexps, to search and replace a substring:
undefinedundefined
undefinedundefinedjs run // replace a dash by a colon alert('12-34-56'.replace("-", ":")) // 12:34-56undefinedundefined
There's a pitfall though.
undefinedundefinedundefinedundefinedWhen the first
argument of undefinedundefinedreplace is a
string, it only replaces the first
match.undefinedundefinedundefinedundefined
You can see that in the example above: only
the first undefinedundefined"-" is replaced by
undefinedundefined":".undefinedundefined
To find all hyphens, we need to use not the
string undefinedundefined"-", but a regexp
undefinedundefinedpattern:/-/g, with the obligatory
undefinedundefinedpattern:g flag:undefinedundefined
undefinedundefinedjs run // replace all dashes by a colon alert( '12-34-56'.replace( *!*/-/g*/!*, ":" ) ) // 12:34:56undefinedundefined
The second argument is a replacement string. We can use special characters in it:
undefinedundefined| Symbols | undefinedundefinedAction in the replacement string | undefinedundefined
|---|---|
undefinedundefined$&undefinedundefined
| undefinedundefinedinserts the whole match | undefinedundefined
undefinedundefinedundefinedundefined$`</code>|inserts a part of the string before the match| |`$'undefinedundefined | undefinedundefined
undefinedundefined |
For instance:
undefinedundefinedrun let str = "John Smith";
undefinedundefined// swap first and last name alert(str.replace(/(john) (smith)/i, ‘$2, $1')) // Smith, John
undefinedundefinedundefinedundefinedFor situations that require "smart" replacements, the second argument can be a function.undefinedundefined
undefinedundefinedIt will be called for each match, and the returned value will be inserted as a replacement.
undefinedundefinedThe
function is called with arguments
undefinedundefinedfunc(match, p1, p2, ..., pn, offset, input, groups):undefinedundefined
match - the
match,undefinedundefinedp1, p2, ..., pn - contents of
capturing groups (if there are any),undefinedundefinedoffset -
position of the match,undefinedundefinedinput - the
source string,undefinedundefinedgroups - an object with named
groups.undefinedundefinedIf there are no parentheses in the regexp,
then there are only 3 arguments:
undefinedundefinedfunc(str, offset, input).undefinedundefined
For example, let's uppercase all matches:
undefinedundefinedrun let str = "html and css";
undefinedundefinedlet result = str.replace(/html|css/gi, str => str.toUpperCase());
undefinedundefinedalert(result); // HTML and CSS
undefinedundefinedReplace each match by its position in the string:
undefinedundefined
undefinedundefinedjs run alert("Ho-Ho-ho".replace(/ho/gi, (match, offset) => offset)); // 0-3-6undefinedundefined
In the example below there are two parentheses, so the replacement function is called with 5 arguments: the first is the full match, then 2 parentheses, and after it (not used in the example) the match position and the source string:
undefinedundefinedrun let str = "John Smith";
undefinedundefinedlet result = str.replace(/(+)
(+)/, (match, name, surname) =>
undefinedundefined${surname}, ${name});undefinedundefined
alert(result); // Smith, John
undefinedundefinedIf there are many groups, it's convenient to use rest parameters to access them:
undefinedundefinedrun let str = "John Smith";
undefinedundefinedlet result =
str.replace(/(+) (+)/, (…match) =>
undefinedundefined${match[2]}, ${match[1]});undefinedundefined
alert(result); // Smith, John
undefinedundefinedOr, if we're using named groups, then
undefinedundefinedgroups object with them is always
the last, so we can obtain it like this:undefinedundefined
run let str = "John Smith";
undefinedundefinedlet result =
str.replace(/(?undefinedundefined
return
undefinedundefined${groups.surname}, ${groups.name};
});undefinedundefined
alert(result); // Smith, John
undefinedundefinedUsing a function gives us the ultimate replacement power, because it gets all the information about the match, has access to outer variables and can do everything.
undefinedundefined
This method is essentially the same as
undefinedundefinedstr.replace, with two major
differences:undefinedundefined
replace
replaces only the undefinedundefinedfirst
occurence.undefinedundefinedg flag, there'll be an error.
With undefinedundefinedg flag, it works the same
as undefinedundefinedreplace.undefinedundefined
The main use
case for undefinedundefinedreplaceAll is replacing
all occurences of a string.undefinedundefined
Like this:
undefinedundefined
undefinedundefinedjs run // replace all dashes by a colon alert('12-34-56'.replaceAll("-", ":")) // 12:34:56undefinedundefined
The method
undefinedundefinedregexp.exec(str) method returns a
match for undefinedundefinedregexp in the string
undefinedundefinedstr. Unlike previous methods,
it's called on a regexp, not on a string.undefinedundefined
It behaves differently depending on whether
the regexp has flag
undefinedundefinedpattern:g.undefinedundefined
If there's no
undefinedundefinedpattern:g, then
undefinedundefinedregexp.exec(str) returns the
first match exactly as
undefinedundefinedstr.match(regexp). This behavior
doesn't bring anything new.undefinedundefined
But if there's flag
undefinedundefinedpattern:g, then: - A call to
undefinedundefinedregexp.exec(str) returns the
first match and saves the position immediately after it in the
property undefinedundefinedregexp.lastIndex. - The
next such call starts the search from position
undefinedundefinedregexp.lastIndex, returns the
next match and saves the position after it in
undefinedundefinedregexp.lastIndex. - …And so on. -
If there are no matches,
undefinedundefinedregexp.exec returns
undefinedundefinednull and resets
undefinedundefinedregexp.lastIndex to
undefinedundefined0.undefinedundefined
So, repeated calls return all matches one
after another, using property
undefinedundefinedregexp.lastIndex to keep track of
the current search position.undefinedundefined
In the past, before the method
undefinedundefinedstr.matchAll was added to
JavaScript, calls of undefinedundefinedregexp.exec
were used in the loop to get all matches with
groups:undefinedundefined
run let str = ‘More about JavaScript at https://javascript.info'; let regexp = /javascript/ig;
undefinedundefinedlet result;
undefinedundefinedwhile (result = regexp.exec(str)) { alert(
undefinedundefinedFound ${result[0]} at position ${result.index}
); // Found JavaScript at position 11, then // Found javascript
at position 33 }
undefinedundefined
This works now as
well, although for newer browsers
undefinedundefinedstr.matchAll is usually more
convenient.undefinedundefined
undefinedundefinedWe can use
undefinedundefinedregexp.exec to search from a
given position by manually setting
undefinedundefinedlastIndex.undefinedundefinedundefinedundefined
For instance:
undefinedundefinedrun let str = ‘Hello, world!''';
undefinedundefinedlet regexp = /+/g; // without flag "g", lastIndex property is ignored regexp.lastIndex = 5; // search from 5th position (from the comma)
undefinedundefinedalert( regexp.exec(str) ); // world
undefinedundefinedIf the regexp has flag
undefinedundefinedpattern:y, then the search will
be performed exactly at the position
undefinedundefinedregexp.lastIndex, not any
further.undefinedundefined
Let's replace
flag undefinedundefinedpattern:g with
undefinedundefinedpattern:y in the example above.
There will be no matches, as there's no word at position
undefinedundefined5:undefinedundefined
run let str = ‘Hello, world!''';
undefinedundefinedlet regexp = /+/y; regexp.lastIndex = 5; // search exactly at position 5
undefinedundefinedalert( regexp.exec(str) ); // null
undefinedundefinedThat's convenient for situations when we need to "read" something from the string by a regexp at the exact position, not somewhere further.
undefinedundefined
The method undefinedundefinedregexp.test(str) looks
for a match and returns
undefinedundefinedtrue/false whether it
exists.undefinedundefined
For instance:
undefinedundefinedrun let str = "I love JavaScript";
undefinedundefined// these two tests do the same alert( undefinedundefined!/love/iundefinedundefined/!.test(str) ); // true alert( str.search(undefinedundefined!/love/iundefinedundefined/!) != -1 ); // true undefinedundefined
undefinedundefinedAn example with the negative answer:
undefinedundefinedrun let str = "Bla-bla-bla";
undefinedundefinedalert( undefinedundefined!/love/iundefinedundefined/!.test(str) ); // false alert( str.search(undefinedundefined!/love/iundefinedundefined/!) != -1 ); // false undefinedundefined
undefinedundefinedIf the regexp has
flag undefinedundefinedpattern:g, then
undefinedundefinedregexp.test looks from
undefinedundefinedregexp.lastIndex property and
updates this property, just like
undefinedundefinedregexp.exec.undefinedundefined
So we can use it to search from a given position:
undefinedundefinedrun let regexp = /love/gi;
undefinedundefinedlet str = "I love JavaScript";
undefinedundefined// start the search from position 10: regexp.lastIndex = 10; alert( regexp.test(str) ); // false (no match)
undefinedundefined
undefinedundefinedwarn header="Same global regexp tested repeatedly on different sources may fail" If we apply the same global regexp to different inputs, it may lead to wrong result, becauseregexp.testundefinedundefinedcall advancesregexp.lastIndex`
property, so the search in another string may start from
non-zero position.undefinedundefined
For
instance, here we call
undefinedundefinedregexp.test twice on the same
text, and the second time fails:undefinedundefined
run let regexp = /javascript/g; // (regexp just created: regexp.lastIndex=0)
undefinedundefinedalert( regexp.test("javascript") ); // true (regexp.lastIndex=10 now) alert( regexp.test("javascript") ); // false
undefinedundefinedundefinedundefined
That's exactly because `regexp.lastIndex` is non-zero in the second test.
To work around that, we can set `regexp.lastIndex = 0` before each search. Or instead of calling methods on regexp, use string methods `str.match/search/...`, they don't use `lastIndex`.undefinedundefined
undefinedundefinedA
undefinedundefinedswitch statement can replace
multiple undefinedundefinedif
checks.undefinedundefined
It gives a more descriptive way to compare a value with multiple variants.
undefinedundefinedThe undefinedundefinedswitch has
one or more undefinedundefinedcase blocks and an
optional default.undefinedundefined
It looks like this:
undefinedundefinedno-beautify switch(x) { case ‘value1': // if (x === ‘value1') … [break]
undefinedundefinedcase ‘value2': // if (x === ‘value2') … [break]
undefinedundefineddefault: … [break] }
undefinedundefinedx is checked for a strict
equality to the value from the first
undefinedundefinedcase (that is,
undefinedundefinedvalue1) then to the second
(undefinedundefinedvalue2) and so
on.undefinedundefinedswitch
starts to execute the code starting from the corresponding
undefinedundefinedcase, until the nearest
undefinedundefinedbreak (or until the end of
undefinedundefinedswitch).undefinedundefineddefault code is executed (if it
exists).undefinedundefinedAn example of
undefinedundefinedswitch (the executed code is
highlighted):undefinedundefined
run let a = 2 + 2;
undefinedundefinedswitch (a) { case 3: alert( ‘Too small' ); break; undefinedundefined! case 4: alert( ‘Exactly!''' ); break; undefinedundefined/! case 5: alert( ‘Too big' ); break; default: alert( "I don't know such values" ); } undefinedundefined
undefinedundefinedHere the
undefinedundefinedswitch starts to compare
undefinedundefineda from the first
undefinedundefinedcase variant that is
undefinedundefined3. The match
fails.undefinedundefined
Then
undefinedundefined4. That's a match, so the
execution starts from undefinedundefinedcase 4
until the nearest
undefinedundefinedbreak.undefinedundefined
undefinedundefinedIf there is no
undefinedundefinedbreak then the execution
continues with the next undefinedundefinedcase
without any
checks.undefinedundefinedundefinedundefined
An example without
undefinedundefinedbreak:undefinedundefined
run let a = 2 + 2;
undefinedundefinedswitch (a) { case 3: alert( ‘Too small' ); undefinedundefined! case 4: alert( ‘Exactly!''' ); case 5: alert( ‘Too big' ); default: alert( "I don't know such values" ); undefinedundefined/! } undefinedundefined
undefinedundefinedIn the example above
we'll see sequential execution of three
undefinedundefinedalerts:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefined'Exactly!' )undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefined'Too big' )undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefined"I don't know such values" )undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedsmart header="Any expression can be aswitch/caseundefinedundefinedargument" Bothswitchundefinedundefinedandcase`
allow arbitrary expressions.undefinedundefined
For example:
undefinedundefinedrun let a = "1"; let b = 0;
undefinedundefinedswitch (+a) { undefinedundefined! case b + 1: alert("this runs, because +a is 1, exactly equals b+1"); break; undefinedundefined/!undefinedundefined
undefinedundefineddefault: alert("this doesn't run"); }
undefinedundefinedundefinedundefinedHere `+a` gives `1`, that's compared with `b + 1` in `case`, and the corresponding code is executed.undefinedundefined
undefinedundefinedSeveral variants of
undefinedundefinedcase which share the same code
can be grouped.undefinedundefined
For
example, if we want the same code to run for
undefinedundefinedcase 3 and
undefinedundefinedcase 5:undefinedundefined
run no-beautify let a = 3;
undefinedundefinedswitch (a) { case 4: alert(‘Right!'''); break;
undefinedundefinedundefinedundefined! case 3: // (undefinedundefined) grouped two cases case 5: alert(‘Wrong!'''); alert("Why don't you take a math class?"); break; /!*undefinedundefined
undefinedundefineddefault: alert(‘The result is strange. Really.'''); }
undefinedundefinedNow both undefinedundefined3
and undefinedundefined5 show the same
message.undefinedundefined
The ability
to "group" cases is a side-effect of how
undefinedundefinedswitch/case works without
undefinedundefinedbreak. Here the execution of
undefinedundefinedcase 3 starts from the line
undefinedundefined(*) and goes through
undefinedundefinedcase 5, because there's no
undefinedundefinedbreak.undefinedundefined
Let's emphasize that the equality check is always strict. The values must be of the same type to match.
undefinedundefinedFor example, let's consider the code:
undefinedundefinedrun let arg = prompt("Enter a value?"); switch (arg) { case ‘0': case ‘1': alert( ‘One or zero' ); break;
undefinedundefinedcase ‘2': alert( ‘Two' ); break;
undefinedundefinedcase 3: alert( ‘Never executes!''' ); break; default: alert( ‘An unknown value' ); }
undefinedundefined0,
undefinedundefined1, the first
undefinedundefinedalert runs.undefinedundefined
2
the second undefinedundefinedalert
runs.undefinedundefined3, the result of the
undefinedundefinedprompt is a string
undefinedundefined"3", which is not strictly
equal undefinedundefined=== to the number
undefinedundefined3. So we've got a dead code in
undefinedundefinedcase 3! The
undefinedundefineddefault variant will
execute.undefinedundefinedQuite often we need to perform a similar action in many places of the script.
undefinedundefinedFor example, we need to show a nice-looking message when a visitor logs in, logs out and maybe somewhere else.
undefinedundefinedFunctions are the main "building blocks" of the program. They allow the code to be called many times without repetition.
undefinedundefinedWe've already seen examples
of built-in functions, like
undefinedundefinedalert(message),
undefinedundefinedprompt(message, default) and
undefinedundefinedconfirm(question). But we can
create functions of our own as well.undefinedundefined
To create a function we can use a undefinedundefinedfunction declaration.undefinedundefined
undefinedundefinedIt looks like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedshowMessage() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefined'Hello everyone!' )undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The
undefinedundefinedfunction keyword goes first, then
goes the undefinedundefinedname of the function, then a
list of undefinedundefinedparameters between the
parentheses (comma-separated, empty in the example above) and
finally the code of the function, also named "the function
body", between curly braces.undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedname(parameters) undefinedundefined{undefinedundefinedundefinedundefined ...undefinedundefinedbody...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Our new function can be called by its
name:
undefinedundefinedshowMessage().undefinedundefined
For instance:
undefinedundefinedrun function showMessage() { alert( ‘Hello everyone!''' ); }
undefinedundefinedundefinedundefined! showMessage(); showMessage(); undefinedundefined/! undefinedundefined
undefinedundefinedThe call
undefinedundefinedshowMessage() executes the code
of the function. Here we will see the message two
times.undefinedundefined
This example clearly demonstrates one of the main purposes of functions: to avoid code duplication.
undefinedundefinedIf we ever need to change the message or the way it is shown, it's enough to modify the code in one place: the function which outputs it.
undefinedundefinedA variable declared inside a function is only visible inside that function.
undefinedundefinedFor example:
undefinedundefinedrun function showMessage() { undefinedundefined! let message = "Hello, I'm JavaScript!"; // local variable undefinedundefined/!undefinedundefined
undefinedundefinedalert( message ); }
undefinedundefinedshowMessage(); // Hello, I'm JavaScript!
undefinedundefinedalert( message ); // <- Error! The variable is local to the function
undefinedundefinedA function can access an outer variable as well, for example:
undefinedundefinedrun no-beautify let undefinedundefined!userNameundefinedundefined/! = ‘John';undefinedundefined
undefinedundefinedfunction showMessage() { let message = ‘Hello,''' + undefinedundefined!userNameundefinedundefined/!; alert(message); }undefinedundefined
undefinedundefinedshowMessage(); // Hello, John
undefinedundefinedThe function has full access to the outer variable. It can modify it as well.
undefinedundefinedFor instance:
undefinedundefinedrun let undefinedundefined!userNameundefinedundefined/! = ‘John';undefinedundefined
undefinedundefinedfunction showMessage() { undefinedundefined!userNameundefinedundefined/! = "Bob"; // (1) changed the outer variableundefinedundefined
undefinedundefinedlet message = ‘Hello,''' + undefinedundefined!userNameundefinedundefined/!; alert(message); }undefinedundefined
undefinedundefinedalert( userName ); // undefinedundefined!Johnundefinedundefined/! before the function callundefinedundefined
undefinedundefinedshowMessage();
undefinedundefinedalert( userName ); // undefinedundefined!Bobundefinedundefined/!, the value was modified by the function undefinedundefined
undefinedundefinedThe outer variable is only used if there's no local one.
undefinedundefinedIf a
same-named variable is declared inside the function then it
undefinedundefinedshadows the outer one. For instance,
in the code below the function uses the local
undefinedundefineduserName. The outer one is
ignored:undefinedundefined
run let userName = ‘John';
undefinedundefinedfunction showMessage() { undefinedundefined! let userName = "Bob"; // declare a local variable undefinedundefined/!undefinedundefined
undefinedundefinedlet message = ‘Hello,''' + userName; // undefinedundefined!Bobundefinedundefined/! alert(message); }undefinedundefined
undefinedundefined// the function will create and use its own userName showMessage();
undefinedundefinedalert( userName ); // undefinedundefined!Johnundefinedundefined/!, unchanged, the function did not access the outer variable undefinedundefined
undefinedundefined
``undefinedundefinedsmart header="Global variables" Variables declared outside of any function, such as the outeruserName`
in the code above, are called
undefinedundefinedglobal.undefinedundefined
Global variables are visible from any function (unless shadowed by locals).
undefinedundefinedIt's a good practice to minimize the use of global variables. Modern code has few or no globals. Most variables reside in their functions. Sometimes though, they can be useful to store project-level data.
undefinedundefinedWe can pass arbitrary data to functions using parameters (also called undefinedundefinedfunction arguments) .undefinedundefined
undefinedundefinedIn
the example below, the function has two parameters:
undefinedundefinedfrom and
undefinedundefinedtext.undefinedundefined
run function showMessage(undefinedundefined!from, textundefinedundefined/!) { // arguments: from, text alert(from + ‘:''' + text); }undefinedundefined
undefinedundefinedundefinedundefined! showMessage(‘Ann', ‘Hello!'''); // Ann: Hello! (*) showMessage(‘Ann', "What's up?"); // Ann: What's up? (**) undefinedundefined/! undefinedundefined
undefinedundefinedWhen the function is
called in lines undefinedundefined(*) and
undefinedundefined(**), the given values are copied
to local variables undefinedundefinedfrom and
undefinedundefinedtext. Then the function uses
them.undefinedundefined
Here's one more
example: we have a variable undefinedundefinedfrom
and pass it to the function. Please note: the function changes
undefinedundefinedfrom, but the change is not seen
outside, because a function always gets a copy of the
value:undefinedundefined
run function showMessage(from, text) {
undefinedundefinedundefinedundefined! from = ‘undefinedundefined''' + from + ''''''; // make "from" look nicer undefinedundefined/!undefinedundefined
undefinedundefinedalert( from + ‘:''' + text ); }
undefinedundefinedlet from = "Ann";
undefinedundefinedshowMessage(from, "Hello"); // undefinedundefinedAnn: Helloundefinedundefined
undefinedundefined// the value of "from" is the same, the function modified a local copy alert( from ); // Ann
undefinedundefinedIf a parameter is not provided, then its
value becomes
undefinedundefinedundefined.undefinedundefined
For instance, the aforementioned function
undefinedundefinedshowMessage(from, text) can be
called with a single argument:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedshowMessage(undefinedundefined"Ann")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That's not an error. Such a call would
output undefinedundefined"*Ann*: undefined".
There's no undefinedundefinedtext, so it's assumed
that
undefinedundefinedtext === undefined.undefinedundefined
If we want to use a "default"
undefinedundefinedtext in this case, then we can
specify it after
undefinedundefined=:undefinedundefined
run function showMessage(from, undefinedundefined!text = "no text given"undefinedundefined/!) { alert( from + ":" + text ); }undefinedundefined
undefinedundefinedshowMessage("Ann"); // Ann: no text given
undefinedundefinedNow if the
undefinedundefinedtext parameter is not passed, it
will get the value
undefinedundefined"no text given"undefinedundefined
Here
undefinedundefined"no text given" is a string, but
it can be a more complex expression, which is only evaluated and
assigned if the parameter is missing. So, this is also
possible:undefinedundefined
undefinedundefinedjs run function showMessage(from, text = anotherFunction()) { // anotherFunction() only executed if no text given // its result becomes the value of text }undefinedundefined
smart header="Evaluation of default parameters" In JavaScript, a default parameter is evaluated every time the function is called without the respective parameter.
undefinedundefinedIn the
example above, undefinedundefinedanotherFunction()
is called every time
undefinedundefinedshowMessage() is called without
the undefinedundefinedtext parameter.
undefinedundefined
Sometimes it makes sense to set default values for parameters not in the function declaration, but at a later stage, during its execution.
undefinedundefinedTo check for an omitted parameter, we can
compare it with
undefinedundefinedundefined:undefinedundefined
run function showMessage(text) { undefinedundefined! if (text === undefined) { text = ‘empty message'; } undefinedundefined/!undefinedundefined
undefinedundefinedalert(text); }
undefinedundefinedshowMessage(); // empty message
undefinedundefined…Or we could use the
undefinedundefined|| operator:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// if text parameter is omitted or "" is passed, set it to 'empty'undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedshowMessage(text) undefinedundefined{undefinedundefinedundefinedundefined text undefinedundefined= text undefinedundefined||undefinedundefined'empty'undefinedundefined;undefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Modern JavaScript engines support the
undefinedundefinednullish coalescing
operatorundefinedundefined??, it's better
when falsy values, such as undefinedundefined0, are
considered regular:undefinedundefined
run // if there's no "count" parameter, show "unknown" function showCount(count) { alert(count ?? "unknown"); }
undefinedundefinedshowCount(0); // 0 showCount(null); // unknown showCount(); // unknown
undefinedundefinedA function can return a value back into the calling code as the result.
undefinedundefinedThe simplest example would be a function that sums two values:
undefinedundefinedrun no-beautify function sum(a, b) { undefinedundefined!returnundefinedundefined/! a + b; }undefinedundefined
undefinedundefinedlet result = sum(1, 2); alert( result ); // 3
undefinedundefinedThe directive
undefinedundefinedreturn can be in any place of the
function. When the execution reaches it, the function stops, and
the value is returned to the calling code (assigned to
undefinedundefinedresult above).undefinedundefined
There may be many occurrences of
undefinedundefinedreturn in a single function. For
instance:undefinedundefined
run function checkAge(age) { if (age >= 18) { undefinedundefined! return true; undefinedundefined/! } else { undefinedundefined! return confirm(‘Do you have permission from your parents?'''); undefinedundefined/! } }undefinedundefined
undefinedundefinedlet age = prompt(‘How old are you?''', 18);
undefinedundefinedif ( checkAge(age) ) { alert( ‘Access granted' ); } else { alert( ‘Access denied' ); }
undefinedundefinedIt is possible to use
undefinedundefinedreturn without a value. That
causes the function to exit immediately.undefinedundefined
For example:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedshowMovie(age) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif ( undefinedundefined!undefinedundefinedcheckAge(age) ) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined alertundefinedundefined(undefinedundefined "Showing you the movie" undefinedundefined)undefinedundefined; // undefinedundefined(*)undefinedundefinedundefinedundefinedundefinedundefined // ...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In the code above, if
undefinedundefinedcheckAge(age) returns
undefinedundefinedfalse, then
undefinedundefinedshowMovie won't proceed to the
undefinedundefinedalert.undefinedundefined
undefinedundefinedsmart header="A function with an emptyreturnundefinedundefinedor without it returnsundefinedundefinedundefined" If a function does not return a value, it is the same as if it returnsundefined`:undefinedundefined
run function doNothing() { /* empty */ }
undefinedundefinedalert( doNothing() === undefined ); // true
undefinedundefinedAn empty
undefinedundefinedreturn is also the same as
undefinedundefinedreturn undefined:undefinedundefined
run function doNothing() { return; }
undefinedundefinedalert( doNothing() === undefined ); // true
undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedwarn header="Never add a newline betweenreturnundefinedundefinedand the value" For a long expression inreturn`,
it might be tempting to put it on a separate line, like
this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedundefinedundefined (some undefinedundefined+ long undefinedundefined+ expression undefinedundefined+ or undefinedundefined+ whatever undefinedundefined*undefinedundefinedf(a) undefinedundefined+undefinedundefinedf(b))undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That doesn't work, because JavaScript
assumes a semicolon after undefinedundefinedreturn.
That'll work the same as:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefined*!*;*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined(undefinedundefinedsome undefinedundefined+undefinedundefined long undefinedundefined+undefinedundefined expression undefinedundefined+undefinedundefined or undefinedundefined+undefinedundefined whatever undefinedundefined*undefinedundefined fundefinedundefined(undefinedundefinedaundefinedundefined)undefinedundefinedundefinedundefined+undefinedundefined fundefinedundefined(undefinedundefinedbundefinedundefined))undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
So, it effectively becomes an empty return.
undefinedundefinedIf we want the returned
expression to wrap across multiple lines, we should start it at
the same line as undefinedundefinedreturn. Or at
least put the opening parentheses there as
follows:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedreturn (undefinedundefinedundefinedundefined some undefinedundefined+ long undefinedundefined+ expressionundefinedundefinedundefinedundefinedundefinedundefined+ or undefinedundefined+undefinedundefinedundefinedundefined whatever undefinedundefined*undefinedundefinedf(a) undefinedundefined+undefinedundefinedf(b)undefinedundefinedundefinedundefined )undefinedundefinedundefinedundefined
undefinedundefined
And it will work just as we expect it to.
undefinedundefinedFunctions are actions. So their name is usually a verb. It should be brief, as accurate as possible and describe what the function does, so that someone reading the code gets an indication of what the function does.
undefinedundefinedIt is a widespread practice to start a function with a verbal prefix which vaguely describes the action. There must be an agreement within the team on the meaning of the prefixes.
undefinedundefinedFor instance, functions that start with
undefinedundefined"show" usually show
something.undefinedundefined
Function starting with…
undefinedundefined"get…" - return a
value,undefinedundefined"calc…" - calculate
something,undefinedundefined"create…" - create
something,undefinedundefined"check…" - check something and
return a boolean, etc.undefinedundefinedExamples of such names:
undefinedundefined
undefinedundefinedjs no-beautify showMessage(..) // shows a message getAge(..) // returns the age (gets it somehow) calcSum(..) // calculates a sum and returns the result createForm(..) // creates a form (and usually returns it) checkPermission(..) // checks a permission, returns true/falseundefinedundefined
With prefixes in place, a glance at a function name gives an understanding what kind of work it does and what kind of value it returns.
undefinedundefinedsmart header="One function - one action" A function should do exactly what is suggested by its name, no more.
undefinedundefinedTwo independent actions usually deserve two functions, even if they are usually called together (in that case we can make a 3rd function that calls those two).
undefinedundefinedA few examples of breaking this rule:
undefinedundefinedgetAge - would be bad if it
shows an undefinedundefinedalert with the age
(should only get).undefinedundefinedcreateForm - would be bad if
it modifies the document, adding a form to it (should only
create it and return).undefinedundefinedcheckPermission - would be bad
if it displays the
undefinedundefinedaccess granted/denied message
(should only perform the check and return the
result).undefinedundefinedThese examples assume common meanings of prefixes. You and your team are free to agree on other meanings, but usually they're not much different. In any case, you should have a firm understanding of what a prefix means, what a prefixed function can and cannot do. All same-prefixed functions should obey the rules. And the team should share the knowledge.
undefinedundefinedsmart header="Ultrashort function names" Functions that are used undefinedundefinedvery often sometimes have ultrashort names.undefinedundefined
undefinedundefinedFor example,
the undefinedundefinedjQuery
framework defines a function with
undefinedundefined$. The undefinedundefinedLodash library has its core
function named
undefinedundefined_.undefinedundefined
These are exceptions. Generally function names should be concise and descriptive.
undefinedundefinedFunctions should be short and do exactly one thing. If that thing is big, maybe it's worth it to split the function into a few smaller functions. Sometimes following this rule may not be that easy, but it's definitely a good thing.
undefinedundefinedA separate function is not only easier to test and debug - its very existence is a great comment!
undefinedundefinedFor instance, compare the two
functions undefinedundefinedshowPrimes(n) below.
Each one outputs undefinedundefinedprime
numbers up to
undefinedundefinedn.undefinedundefined
The first variant uses a label:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedshowPrimes(n) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednextPrimeundefinedundefined:undefinedundefinedfor (undefinedundefinedlet i undefinedundefined=undefinedundefined2undefinedundefined; i undefinedundefined< nundefinedundefined; iundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedlet j undefinedundefined=undefinedundefined2undefinedundefined; j undefinedundefined< iundefinedundefined; jundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (i undefinedundefined% j undefinedundefined==undefinedundefined0) undefinedundefinedcontinue nextPrimeundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedalert( i )undefinedundefined;undefinedundefined// a primeundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The second variant uses an additional
function undefinedundefinedisPrime(n) to test for
primality:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedshowPrimes(n) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedlet i undefinedundefined=undefinedundefined2undefinedundefined; i undefinedundefined< nundefinedundefined; iundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedif (undefinedundefined!undefinedundefinedisPrime(i)) undefinedundefinedcontinueundefinedundefined;*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined alertundefinedundefined(undefinedundefinediundefinedundefined)undefinedundefined; // a primeundefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunction isPrimeundefinedundefined(undefinedundefinednundefinedundefined)undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined for undefinedundefined(undefinedundefinedlet i = 2; i < n; iundefinedundefined++)undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined if undefinedundefined(undefinedundefined n % i == 0undefinedundefined)undefinedundefined return false;undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefined return true;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The second variant is easier to
understand, isn't it? Instead of the code piece we see a name of
the action (undefinedundefinedisPrime). Sometimes
people refer to such code as
undefinedundefinedself-describing.undefinedundefined
So, functions can be created even if we don't intend to reuse them. They structure the code and make it readable.
undefinedundefinedA function declaration looks like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedname(parametersundefinedundefined, delimitedundefinedundefined, byundefinedundefined, comma) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined/* code */undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefined.undefinedundefined
To make the code clean and easy to understand, it's recommended to use mainly local variables and parameters in the function, not outer variables.
undefinedundefinedIt is always easier to understand a function which gets parameters, works with them and returns a result than a function which gets no parameters, but modifies outer variables as a side-effect.
undefinedundefinedFunction naming:
undefinedundefinedcreate…,
undefinedundefinedshow…,
undefinedundefinedget…,
undefinedundefinedcheck… and so on. Use them to
hint what a function does.undefinedundefinedFunctions are the main building blocks of scripts. Now we've covered the basics, so we actually can start creating and using them. But that's only the beginning of the path. We are going to return to them many times, going more deeply into their advanced features.
undefinedundefinedThis book is a undefinedundefinedtutorial. It aims to help you gradually learn the language. But once you're familiar with the basics, you'll need other sources.undefinedundefined
undefinedundefinedundefinedundefinedThe ECMA-262 specification contains the most in-depth, detailed and formalized information about JavaScript. It defines the language.undefinedundefined
undefinedundefinedBut being that formalized, it's difficult to understand at first. So if you need the most trustworthy source of information about the language details, the specification is the right place. But it's not for everyday use.
undefinedundefinedA new specification version is released every year. In-between these releases, the latest specification draft is at undefinedundefinedhttps://tc39.es/ecma262/.undefinedundefined
undefinedundefinedTo read about new bleeding-edge features, including those that are "almost standard" (so-called "stage 3"), see proposals at undefinedundefinedhttps://github.com/tc39/proposals.undefinedundefined
undefinedundefinedAlso, if you're developing for the browser, then there are other specifications covered in the undefinedundefinedsecond part of the tutorial.undefinedundefined
undefinedundefinedundefinedundefinedMDN (Mozilla) JavaScript Reference is the main manual with examples and other information. It's great to get in-depth information about individual language functions, methods etc.undefinedundefined
undefinedundefinedOne can find it at undefinedundefinedhttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference.undefinedundefined
undefinedundefinedAlthough, it's often best to use an internet
search instead. Just use "MDN [term]" in the query,
e.g. undefinedundefinedhttps://google.com/search?q=MDN+parseInt to
search for undefinedundefinedparseInt
function.undefinedundefined
JavaScript is a developing language, new features get added regularly.
undefinedundefinedTo see their support among browser-based and other engines, see:
undefinedundefinedAll these resources are useful in real-life development, as they contain valuable information about language details, their support etc.
undefinedundefinedPlease remember them (or this page) for the cases when you need in-depth information about a particular feature.
undefinedundefinedIn JavaScript, a function is not a "magical language structure", but a special kind of value.
undefinedundefinedThe syntax that we used before is called a undefinedundefinedFunction Declaration:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedsayHi() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefined"Hello" )undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
There is another syntax for creating a function that is called a undefinedundefinedFunction Expression.undefinedundefined
undefinedundefinedIt looks like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet sayHi undefinedundefined=undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefined"Hello" )undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Here, the function is created and
assigned to the variable explicitly, like any other value. No
matter how the function is defined, it's just a value stored in
the variable
undefinedundefinedsayHi.undefinedundefined
The meaning of these code samples is the
same: "create a function and put it into the variable
undefinedundefinedsayHi".undefinedundefined
We can even print out that value using
undefinedundefinedalert:undefinedundefined
run function sayHi() { alert( "Hello" ); }
undefinedundefinedundefinedundefined! alert( sayHi ); // shows the function code undefinedundefined/! undefinedundefined
undefinedundefinedPlease note that the
last line does not run the function, because there are no
parentheses after undefinedundefinedsayHi. There
are programming languages where any mention of a function name
causes its execution, but JavaScript is not like
that.undefinedundefined
In JavaScript, a function is a value, so we can deal with it as a value. The code above shows its string representation, which is the source code.
undefinedundefinedSurely, a function is a special value, in
the sense that we can call it like
undefinedundefinedsayHi().undefinedundefined
But it's still a value. So we can work with it like with other kinds of values.
undefinedundefinedWe can copy a function to another variable:
undefinedundefinedrun no-beautify function sayHi() { // (1) create alert( "Hello" ); }
undefinedundefinedlet func = sayHi; // (2) copy
undefinedundefinedfunc(); // Hello // (3) run the copy (it works)! sayHi(); // Hello // this still works too (why wouldn't it)
undefinedundefinedHere's what happens above in detail:
undefinedundefined(1) creates the
function and puts it into the variable named
undefinedundefinedsayHi.undefinedundefined(2)
copies it into the variable
undefinedundefinedfunc. Please note again: there
are no parentheses after undefinedundefinedsayHi.
If there were, then
undefinedundefinedfunc = sayHi() would write
undefinedundefinedthe result of the
callundefinedundefinedsayHi() into
undefinedundefinedfunc, not
undefinedundefinedthe
functionundefinedundefinedsayHi
itself.undefinedundefinedsayHi() and
undefinedundefinedfunc().undefinedundefinedNote that we could
also have used a Function Expression to declare
undefinedundefinedsayHi, in the first
line:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet sayHi undefinedundefined=undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefined"Hello" )undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet func undefinedundefined= sayHiundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Everything would work the same.
undefinedundefined
undefinedundefinedsmart header="Why is there a semicolon at the end?" You might wonder, why does Function Expression have a semicolon;`
at the end, but Function Declaration does not:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedsayHi() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet sayHi undefinedundefined=undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined}*!*;*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The answer is simple: - There's no need
for undefinedundefined; at the end of code blocks
and syntax structures that use them like
undefinedundefinedif { ... },
undefinedundefinedfor { },
undefinedundefinedfunction f { } etc. - A Function
Expression is used inside the statement:
undefinedundefinedlet sayHi = ...;, as a value.
It's not a code block, but rather an assignment. The semicolon
undefinedundefined; is recommended at the end of
statements, no matter what the value is. So the semicolon here
is not related to the Function Expression itself, it just
terminates the statement.
undefinedundefined
Let's look at more examples of passing functions as values and using function expressions.
undefinedundefinedWe'll write a function
undefinedundefinedask(question, yes, no) with three
parameters:undefinedundefined
questionundefinedundefinedyesundefinedundefinednoundefinedundefinedThe function
should ask the undefinedundefinedquestion and,
depending on the user's answer, call
undefinedundefinedyes() or
undefinedundefinedno():undefinedundefined
run undefinedundefined! function ask(question, yes, no) { if (confirm(question)) yes() else no(); } undefinedundefined/!undefinedundefined
undefinedundefinedfunction showOk() { alert( "You agreed." ); }
undefinedundefinedfunction showCancel() { alert( "You canceled the execution." ); }
undefinedundefined// usage: functions showOk, showCancel are passed as arguments to ask ask("Do you agree?", showOk, showCancel);
undefinedundefinedIn practice, such functions are quite
useful. The major difference between a real-life
undefinedundefinedask and the example above is that
real-life functions use more complex ways to interact with the
user than a simple undefinedundefinedconfirm. In
the browser, such function usually draws a nice-looking question
window. But that's another story.undefinedundefined
undefinedundefinedThe arguments
undefinedundefinedshowOk and
undefinedundefinedshowCancel of
undefinedundefinedask are called
undefinedundefinedcallback functions or just
undefinedundefinedcallbacks.undefinedundefinedundefinedundefined
The idea is that we pass a function and
expect it to be "called back" later if necessary. In our case,
undefinedundefinedshowOk becomes the callback for
"yes" answer, and undefinedundefinedshowCancel for
"no" answer.undefinedundefined
We can use Function Expressions to write the same function much shorter:
undefinedundefinedrun no-beautify function ask(question, yes, no) { if (confirm(question)) yes() else no(); }
undefinedundefinedundefinedundefined! ask( "Do you agree?", function() { alert("You agreed."); }, function() { alert("You canceled the execution."); } ); undefinedundefined/! undefinedundefined
undefinedundefinedHere, functions are
declared right inside the
undefinedundefinedask(...) call. They have no name,
and so are called undefinedundefinedanonymous. Such
functions are not accessible outside of
undefinedundefinedask (because they are not
assigned to variables), but that's just what we want
here.undefinedundefined
Such code appears in our scripts very naturally, it's in the spirit of JavaScript.
undefinedundefinedsmart header="A function is a value representing an "action"" Regular values like strings or numbers represent the undefinedundefineddata.undefinedundefined
undefinedundefinedA function can be perceived as an undefinedundefinedaction.undefinedundefined
undefinedundefinedWe can pass it between variables and run when we want.
undefinedundefinedLet's formulate the key differences between Function Declarations and Expressions.
undefinedundefinedFirst, the syntax: how to differentiate between them in the code.
undefinedundefinedundefinedundefinedFunction Declaration: a function, declared as a separate statement, in the main code flow.undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// Function Declarationundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedsum(aundefinedundefined, b) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturn a undefinedundefined+ bundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedFunction
Expression: a function, created inside an expression
or inside another syntax construct. Here, the function is
created at the right side of the "assignment expression"
undefinedundefined=:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// Function Expressionundefinedundefinedundefinedundefinedundefinedundefinedlet sum undefinedundefined=undefinedundefinedfunction(aundefinedundefined, b) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturn a undefinedundefined+ bundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The more subtle difference is undefinedundefinedwhen a function is created by the JavaScript engine.undefinedundefined
undefinedundefinedundefinedundefinedA Function Expression is created when the execution reaches it and is usable only from that moment.undefinedundefined
undefinedundefined
Once the execution flow passes to the right side of the
assignment undefinedundefinedlet sum = function… -
here we go, the function is created and can be used (assigned,
called, etc. ) from now on.undefinedundefined
Function Declarations are different.
undefinedundefinedundefinedundefinedA Function Declaration can be called earlier than it is defined.undefinedundefined
undefinedundefinedFor example, a global Function Declaration is visible in the whole script, no matter where it is.
undefinedundefinedThat's due to internal algorithms. When JavaScript prepares to run the script, it first looks for global Function Declarations in it and creates the functions. We can think of it as an "initialization stage".
undefinedundefinedAnd after all Function Declarations are processed, the code is executed. So it has access to these functions.
undefinedundefinedFor example, this works:
undefinedundefinedrun refresh untrusted undefinedundefined! sayHi("John"); // Hello, John undefinedundefined/!undefinedundefined
undefinedundefinedfunction sayHi(name) { alert(
undefinedundefinedHello, ${name} ); }
undefinedundefined
The Function
Declaration undefinedundefinedsayHi is created when
JavaScript is preparing to start the script and is visible
everywhere in it.undefinedundefined
…If it were a Function Expression, then it wouldn't work:
undefinedundefinedrun refresh untrusted undefinedundefined! sayHi("John"); // error! undefinedundefined/!undefinedundefined
undefinedundefinedlet sayHi = function(name) { // (*) no magic
any more alert( undefinedundefinedHello, ${name} );
};
undefinedundefined
Function Expressions
are created when the execution reaches them. That would happen
only in the line undefinedundefined(*). Too
late.undefinedundefined
Another special feature of Function Declarations is their block scope.
undefinedundefinedundefinedundefinedIn strict mode, when a Function Declaration is within a code block, it's visible everywhere inside that block. But not outside of it.undefinedundefined
undefinedundefinedFor
instance, let's imagine that we need to declare a function
undefinedundefinedwelcome() depending on the
undefinedundefinedage variable that we get during
runtime. And then we plan to use it some time
later.undefinedundefined
If we use Function Declaration, it won't work as intended:
undefinedundefinedrun let age = prompt("What is your age?", 18);
undefinedundefined// conditionally declare a function if (age < 18) {
undefinedundefinedfunction welcome() { alert("Hello!"); }
undefinedundefined} else {
undefinedundefinedfunction welcome() { alert("Greetings!"); }
undefinedundefined}
undefinedundefined// …use it later undefinedundefined! welcome(); // Error: welcome is not defined undefinedundefined/! undefinedundefined
undefinedundefinedThat's because a Function Declaration is only visible inside the code block in which it resides.
undefinedundefinedHere's another example:
undefinedundefinedrun let age = 16; // take 16 as an example
undefinedundefinedif (age < 18) {
undefinedundefined! welcome(); // (runs)
undefinedundefined/! // | function welcome() { //
|undefinedundefined
alert("Hello!"); // | Function Declaration is available } // |
everywhere in the block where it's declared // |
undefinedundefined! welcome(); // / (runs)
undefinedundefined/!undefinedundefined
} else {
undefinedundefinedfunction
welcome() {undefinedundefined
alert("Greetings!"); } }undefinedundefined
// Here we're out of curly braces, // so we can not see Function Declarations made inside of them.
undefinedundefinedundefinedundefined! welcome(); // Error: welcome is not defined undefinedundefined/! undefinedundefined
undefinedundefinedWhat can we do to
make undefinedundefinedwelcome visible outside of
undefinedundefinedif?undefinedundefined
The correct approach would be to use a
Function Expression and assign
undefinedundefinedwelcome to the variable that is
declared outside of undefinedundefinedif and has
the proper visibility.undefinedundefined
This code works as intended:
undefinedundefinedrun let age = prompt("What is your age?", 18);
undefinedundefinedlet welcome;
undefinedundefinedif (age < 18) {
undefinedundefinedwelcome = function() { alert("Hello!"); };
undefinedundefined} else {
undefinedundefinedwelcome = function() { alert("Greetings!"); };
undefinedundefined}
undefinedundefinedundefinedundefined! welcome(); // ok now undefinedundefined/! undefinedundefined
undefinedundefinedOr we could simplify
it even further using a question mark operator
undefinedundefined?:undefinedundefined
run let age = prompt("What is your age?", 18);
undefinedundefinedlet welcome = (age < 18) ? function() { alert("Hello!"); } : function() { alert("Greetings!"); };
undefinedundefinedundefinedundefined! welcome(); // ok now undefinedundefined/! undefinedundefined
undefinedundefinedsmart header="When to choose Function Declaration versus Function Expression?" As a rule of thumb, when we need to declare a function, the first to consider is Function Declaration syntax. It gives more freedom in how to organize our code, because we can call such functions before they are declared.
undefinedundefinedThat's also better for
readability, as it's easier to look up
undefinedundefinedfunction f(…) {…} in the code
than undefinedundefinedlet f = function(…) {…};.
Function Declarations are more "eye-catching".undefinedundefined
…But if a Function Declaration does not suit us for some reason, or we need a conditional declaration (we've just seen an example), then Function Expression should be used.
undefinedundefinedIn most cases when we need to declare a function, a Function Declaration is preferable, because it is visible prior to the declaration itself. That gives us more flexibility in code organization, and is usually more readable.
undefinedundefinedSo we should use a Function Expression only when a Function Declaration is not fit for the task. We've seen a couple of examples of that in this chapter, and will see more in the future.
undefinedundefinedThere's another very simple and concise syntax for creating functions, that's often better than Function Expressions.
undefinedundefinedIt's called "arrow functions", because it looks like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet func undefinedundefined= (arg1undefinedundefined, arg2undefinedundefined, ...undefinedundefined, argN) undefinedundefined=> expressionundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…This creates a function
undefinedundefinedfunc that accepts arguments
undefinedundefinedarg1..argN, then evaluates the
undefinedundefinedexpression on the right side with
their use and returns its result.undefinedundefined
In other words, it's the shorter version of:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet func undefinedundefined=undefinedundefinedfunction(arg1undefinedundefined, arg2undefinedundefined, ...undefinedundefined, argN) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturn expressionundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Let's see a concrete example:
undefinedundefinedrun let sum = (a, b) => a + b;
undefinedundefined/* This arrow function is a shorter form of:
undefinedundefinedlet sum = function(a, b) { return a + b; }; */
undefinedundefinedalert( sum(1, 2) ); // 3
undefinedundefinedAs you can, see
undefinedundefined(a, b) => a + b means a
function that accepts two arguments named
undefinedundefineda and
undefinedundefinedb. Upon the execution, it
evaluates the expression undefinedundefineda + b
and returns the result.undefinedundefined
If we have only one argument, then parentheses around parameters can be omitted, making that even shorter.
undefinedundefinedFor example:
undefinedundefinedrun undefinedundefined! let double = n => n * 2; // roughly the same as: let double = function(n) { return n * 2 } undefinedundefined/!undefinedundefined
undefinedundefinedalert( double(3) ); // 6
undefinedundefinedIf there are no arguments, parentheses will be empty (but they should be present):
undefinedundefinedrun let sayHi = () => alert("Hello!");
undefinedundefinedsayHi();
undefinedundefinedArrow functions can be used in the same way as Function Expressions.
undefinedundefinedFor instance, to dynamically create a function:
undefinedundefinedrun let age = prompt("What is your age?", 18);
undefinedundefinedlet welcome = (age < 18) ? () => alert(‘Hello') : () => alert("Greetings!");
undefinedundefinedwelcome();
undefinedundefinedArrow functions may appear unfamiliar and not very readable at first, but that quickly changes as the eyes get used to the structure.
undefinedundefinedThey are very convenient for simple one-line actions, when we're just too lazy to write many words.
undefinedundefinedThe examples above took arguments from the
left of undefinedundefined=> and evaluated the
right-side expression with them.undefinedundefined
Sometimes we need something a little bit more
complex, like multiple expressions or statements. It is also
possible, but we should enclose them in curly braces. Then use a
normal undefinedundefinedreturn within
them.undefinedundefined
Like this:
undefinedundefinedrun let sum = (a, b) => { // the curly brace opens a multiline function let result = a + b; undefinedundefined! return result; // if we use curly braces, then we need an explicit "return" undefinedundefined/! };undefinedundefined
undefinedundefinedalert( sum(1, 2) ); // 3
undefinedundefinedsmart header="More to come" Here we praised arrow functions for brevity. But that's not all!
undefinedundefinedArrow functions have other interesting features.
undefinedundefinedTo study them in-depth, we first need to get to know some other aspects of JavaScript, so we'll return to arrow functions later in the chapter undefinedundefinedinfo:arrow-functions.undefinedundefined
undefinedundefinedFor now, we can already use arrow functions for one-line actions and callbacks.
undefinedundefinedArrow functions are handy for one-liners. They come in two flavors:
undefinedundefined(...args) => expression -
the right side is an expression: the function evaluates it and
returns the result.undefinedundefined(...args) => { body } -
brackets allow us to write multiple statements inside the
function, but we need an explicit
undefinedundefinedreturn to return
something.undefinedundefinedThis chapter briefly recaps the features of JavaScript that we've learned by now, paying special attention to subtle moments.
undefinedundefinedStatements are delimited with a semicolon:
undefinedundefined
undefinedundefinedjs run no-beautify alert('Hello'); alert('World');undefinedundefined
Usually, a line-break is also treated as a delimiter, so that would also work:
undefinedundefined
undefinedundefinedjs run no-beautify alert('Hello') alert('World')undefinedundefined
That's called "automatic semicolon insertion". Sometimes it doesn't work, for instance:
undefinedundefinedrun alert("There will be an error after this message")
undefinedundefined[1, 2].forEach(alert)
undefinedundefinedMost codestyle guides agree that we should put a semicolon after each statement.
undefinedundefinedSemicolons are not required after code blocks
undefinedundefined{...} and syntax constructs with
them like loops:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedf() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// no semicolon needed after function declarationundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfor(undefinedundefined;;) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// no semicolon needed after the loopundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…But even if we can put an "extra" semicolon somewhere, that's not an error. It will be ignored.
undefinedundefinedMore in: undefinedundefinedinfo:structure.undefinedundefined
undefinedundefinedTo fully enable all features of modern
JavaScript, we should start scripts with
undefinedundefined"use strict".undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined'use strict'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined...undefinedundefinedundefinedundefined
undefinedundefined
The directive must be at the top of a script or at the beginning of a function body.
undefinedundefinedWithout
undefinedundefined"use strict", everything still
works, but some features behave in the old-fashion, "compatible"
way. We'd generally prefer the modern
behavior.undefinedundefined
Some modern features of the language (like classes that we'll study in the future) enable strict mode implicitly.
undefinedundefinedMore in: undefinedundefinedinfo:strict-mode.undefinedundefined
undefinedundefinedCan be declared using:
undefinedundefinedletundefinedundefinedconst
(constant, can't be changed)undefinedundefinedvar
(old-style, will see later)undefinedundefinedA variable name can
include: - Letters and digits, but the first character may not
be a digit. - Characters undefinedundefined$ and
undefinedundefined_ are normal, on par with
letters. - Non-Latin alphabets and hieroglyphs are also allowed,
but commonly not used.undefinedundefined
Variables are dynamically typed. They can store any value:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet x undefinedundefined=undefinedundefined5undefinedundefined;undefinedundefinedundefinedundefinedx undefinedundefined=undefinedundefined"John"undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
There are 8 data types:
undefinedundefinednumber for both floating-point
and integer numbers,undefinedundefinedbigint for integer numbers of
arbitrary length,undefinedundefinedstring for
strings,undefinedundefinedboolean for logical values:
undefinedundefinedtrue/false,undefinedundefined
null -
a type with a single value
undefinedundefinednull, meaning "empty" or "does
not exist",undefinedundefinedundefined - a type with a
single value undefinedundefinedundefined, meaning
"not assigned",undefinedundefinedobject and
undefinedundefinedsymbol - for complex data
structures and unique identifiers, we haven't learnt them
yet.undefinedundefinedThe undefinedundefinedtypeof
operator returns the type for a value, with two
exceptions:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedtypeofundefinedundefinednullundefinedundefined==undefinedundefined"object"undefinedundefined// error in the languageundefinedundefinedundefinedundefinedundefinedundefinedtypeofundefinedundefinedfunction()undefinedundefined{}undefinedundefined==undefinedundefined"function"undefinedundefined// functions are treated speciallyundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
More in: undefinedundefinedinfo:variables and undefinedundefinedinfo:types.undefinedundefined
undefinedundefinedWe're using a browser as a working environment, so basic UI functions will be:
undefinedundefinedprompt(question, [default])undefinedundefinedundefinedundefined
question, and return either
what the visitor entered or
undefinedundefinednull if they clicked "cancel".
undefinedundefinedconfirm(question)undefinedundefinedundefinedundefined
question and suggest to choose
between Ok and Cancel. The choice is returned as
undefinedundefinedtrue/false.
undefinedundefinedalert(message)undefinedundefinedundefinedundefined
message.
undefinedundefinedAll these functions are undefinedundefinedmodal, they pause the code execution and prevent the visitor from interacting with the page until they answer.undefinedundefined
undefinedundefinedFor instance:
undefinedundefinedrun let userName = prompt("Your name?", "Alice"); let isTeaWanted = confirm("Do you want some tea?");
undefinedundefinedalert( "Visitor:" + userName ); // Alice alert( "Tea wanted:" + isTeaWanted ); // true
undefinedundefinedMore in: undefinedundefinedinfo:alert-prompt-confirm.undefinedundefined
undefinedundefinedJavaScript supports the following operators:
undefinedundefinedRegular:
undefinedundefined* + - /, also
undefinedundefined% for the remainder and
undefinedundefined** for power of a
number.undefinedundefined
The binary
plus undefinedundefined+ concatenates strings.
And if any of the operands is a string, the other one is
converted to string too:undefinedundefined
undefinedundefinedjs run alert( '1' + 2 ); // '12', string alert( 1 + '2' ); // '12', stringundefinedundefined
a = b and combined ones like
undefinedundefineda *= 2.
undefinedundefinedcond ? resultA : resultB. If
undefinedundefinedcond is truthy, returns
undefinedundefinedresultA, otherwise
undefinedundefinedresultB.
undefinedundefined&& and OR
undefinedundefined|| perform short-circuit
evaluation and then return the value where it stopped (not
necessary
undefinedundefinedtrue/undefinedundefinedfalse).
Logical NOT undefinedundefined! converts the
operand to boolean type and returns the inverse value.
undefinedundefined?? operator provides a way to
choose a defined value from a list of variables. The result of
undefinedundefineda ?? b is
undefinedundefineda unless it's
undefinedundefinednull/undefined, then
undefinedundefinedb.
undefinedundefinedEquality check
undefinedundefined== for values of different
types converts them to a number (except
undefinedundefinednull and
undefinedundefinedundefined that equal each
other and nothing else), so these are
equal:undefinedundefined
undefinedundefinedjs run alert( 0 == false ); // true alert( 0 == '' ); // trueundefinedundefined
Other comparisons convert to a number as well.
undefinedundefinedThe strict equality
operator undefinedundefined=== doesn't do the
conversion: different types always mean different values for
it.undefinedundefined
Values
undefinedundefinednull and
undefinedundefinedundefined are special: they
equal undefinedundefined== each other and don't
equal anything else.undefinedundefined
Greater/less comparisons compare strings character-by-character, other types are converted to a number.
undefinedundefinedMore in: undefinedundefinedinfo:operators, undefinedundefinedinfo:comparison, undefinedundefinedinfo:logical-operators, undefinedundefinedinfo:nullish-coalescing-operator.undefinedundefined
undefinedundefinedWe covered 3 types of loops:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// 1undefinedundefinedundefinedundefinedundefinedundefinedwhile (condition) undefinedundefined{undefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// 2undefinedundefinedundefinedundefinedundefinedundefineddoundefinedundefined{undefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined}undefinedundefinedwhile (condition)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// 3undefinedundefinedundefinedundefinedundefinedundefinedfor(undefinedundefinedlet i undefinedundefined=undefinedundefined0undefinedundefined; i undefinedundefined<undefinedundefined10undefinedundefined; iundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
for(let...) loop is visible
only inside the loop. But we can also omit
undefinedundefinedlet and reuse an existing
variable.undefinedundefinedDirectives
undefinedundefinedbreak/continue allow to exit
the whole loop/current iteration. Use labels to break nested
loops.undefinedundefined
Details in: undefinedundefinedinfo:while-for.undefinedundefined
undefinedundefinedLater we'll study more types of loops to deal with objects.
undefinedundefinedThe "switch" construct can replace multiple
undefinedundefinedif checks. It uses
undefinedundefined=== (strict equality) for
comparisons.undefinedundefined
For instance:
undefinedundefinedrun let age = prompt(‘Your age?''', 18);
undefinedundefinedswitch (age) { case 18: alert("Won't work"); // the result of prompt is a string, not a number break;
undefinedundefinedcase "18": alert("This works!"); break;
undefinedundefineddefault: alert("Any value not equal to one above"); }
undefinedundefinedDetails in: undefinedundefinedinfo:switch.undefinedundefined
undefinedundefinedWe covered three ways to create a function in JavaScript:
undefinedundefinedFunction Declaration: the function in the main code flow
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedsum(aundefinedundefined, b) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet result undefinedundefined= a undefinedundefined+ bundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedreturn resultundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Function Expression: the function in the context of an expression
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet sum undefinedundefined=undefinedundefinedfunction(aundefinedundefined, b) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet result undefinedundefined= a undefinedundefined+ bundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedreturn resultundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Arrow functions:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// expression at the right sideundefinedundefinedundefinedundefinedundefinedundefinedlet sum undefinedundefined= (aundefinedundefined, b) undefinedundefined=> a undefinedundefined+ bundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// or multi-line syntax with { ... }, need return here:undefinedundefinedundefinedundefinedundefinedundefinedlet sum undefinedundefined= (aundefinedundefined, b) undefinedundefined=>undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefinedreturn a undefinedundefined+ bundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// without argumentsundefinedundefinedundefinedundefinedundefinedundefinedlet sayHi undefinedundefined= () undefinedundefined=>undefinedundefinedalert(undefinedundefined"Hello")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// with a single argumentundefinedundefinedundefinedundefinedundefinedundefinedlet double undefinedundefined= n undefinedundefined=> n undefinedundefined*undefinedundefined2undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
function sum(a = 1, b = 2) {...}.undefinedundefined
return statement,
then the result is
undefinedundefinedundefined.undefinedundefined
Details: see undefinedundefinedinfo:function-basics, undefinedundefinedinfo:arrow-functions-basics.undefinedundefined
undefinedundefinedThat was a brief list of JavaScript features. As of now we've studied only basics. Further in the tutorial you'll find more specials and advanced features of JavaScript.
undefinedundefinedBefore writing more complex code, let's talk about debugging.
undefinedundefinedundefinedundefinedDebugging is the process of finding and fixing errors within a script. All modern browsers and most other environments support debugging tools - a special UI in developer tools that makes debugging much easier. It also allows to trace the code step by step to see what exactly is going on.undefinedundefined
undefinedundefinedWe'll be using Chrome here, because it has enough features, most other browsers have a similar process.
undefinedundefinedYour Chrome version may look a little bit different, but it still should be obvious what's there.
undefinedundefinedkey:F12 (Mac:
undefinedundefinedkey:Cmd+Opt+I).undefinedundefined
Sources
panel.undefinedundefinedHere's what you should see if you are doing it for the first time:
undefinedundefined
undefinedundefinedundefinedundefined
The toggler button undefinedundefined opens the tab with files.undefinedundefined
undefinedundefinedLet's
click it and select undefinedundefinedhello.js in
the tree view. Here's what should show up:undefinedundefined
undefinedundefinedundefinedundefined
The Sources panel has 3 parts:
undefinedundefinedNow you could click the same toggler undefinedundefined again to hide the resources list and give the code some space.undefinedundefined
undefinedundefinedIf we press
undefinedundefinedkey:Esc, then a console opens
below. We can type commands there and press
undefinedundefinedkey:Enter to
execute.undefinedundefined
After a statement is executed, its result is shown below.
undefinedundefinedFor example, here
undefinedundefined1+2 results in
undefinedundefined3, and
undefinedundefinedhello("debugger") returns
nothing, so the result is
undefinedundefinedundefined:undefinedundefined
undefinedundefinedundefinedundefined
Let's examine what's going on within the code
of the undefinedundefinedexample
page. In undefinedundefinedhello.js, click at
line number undefinedundefined4. Yes, right on the
undefinedundefined4 digit, not on the
code.undefinedundefined
Congratulations!
You've set a breakpoint. Please also click on the number for
line undefinedundefined8.undefinedundefined
It should look like this (blue is where you should click):
undefinedundefinedundefinedundefinedundefinedundefined
A undefinedundefinedbreakpoint is a point of code where the debugger will automatically pause the JavaScript execution.undefinedundefined
undefinedundefinedWhile the code is paused, we can examine current variables, execute commands in the console etc. In other words, we can debug it.
undefinedundefinedWe can always find a list of breakpoints in the right panel. That's useful when we have many breakpoints in various files. It allows us to: - Quickly jump to the breakpoint in the code (by clicking on it in the right panel). - Temporarily disable the breakpoint by unchecking it. - Remove the breakpoint by right-clicking and selecting Remove. - …And so on.
undefinedundefinedsmart header="Conditional breakpoints" undefinedundefinedRight click on the line number allows to create a undefinedundefinedconditional breakpoint. It only triggers when the given expression is truthy.undefinedundefined
undefinedundefinedThat's handy when we need to stop only for a certain variable value or for certain function parameters.
undefinedundefinedWe can also pause the code by using the
undefinedundefineddebugger command in it, like
this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedhello(name) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet phrase undefinedundefined=undefinedundefined`Hello, undefinedundefined${nameundefinedundefined}undefinedundefined!`undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefineddebuggerundefinedundefined;undefinedundefined// <-- the debugger stops hereundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined sayundefinedundefined(undefinedundefinedphraseundefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That's very convenient when we are in a code editor and don't want to switch to the browser and look up the script in developer tools to set the breakpoint.
undefinedundefinedIn our example,
undefinedundefinedhello() is called during the page
load, so the easiest way to activate the debugger (after we've
set the breakpoints) is to reload the page. So let's press
undefinedundefinedkey:F5 (Windows, Linux) or
undefinedundefinedkey:Cmd+R
(Mac).undefinedundefined
As the breakpoint is set, the execution pauses at the 4th line:
undefinedundefinedundefinedundefinedundefinedundefined
Please open the informational dropdowns to the right (labeled with arrows). They allow you to examine the current code state:
undefinedundefined
undefinedundefinedundefinedundefinedWatch
- shows current values for any
expressions.undefinedundefinedundefinedundefined
You can click the plus
undefinedundefined+ and input an expression.
The debugger will show its value at any moment,
automatically recalculating it in the process of
execution.undefinedundefined
undefinedundefinedundefinedundefinedCall Stack
- shows the nested calls
chain.undefinedundefinedundefinedundefined
At the current moment the debugger is
inside undefinedundefinedhello() call, called
by a script in undefinedundefinedindex.html (no
function there, so it's called
"anonymous").undefinedundefined
undefinedundefinedundefinedundefinedScope
- current
variables.undefinedundefinedundefinedundefined
undefinedundefinedLocal
shows local function variables. You can also see their
values highlighted right over the source.undefinedundefined
undefinedundefinedGlobal
has global variables (out of any
functions).undefinedundefined
There's also undefinedundefinedthis keyword
there that we didn't study yet, but we'll do that
soon.undefinedundefined
Now it's time to undefinedundefinedtrace the script.undefinedundefined
undefinedundefinedThere are buttons for it at the top of
the right panel. Let's engage them. undefinedundefined
undefinedundefined - "Resume":
continue the execution, hotkey
undefinedundefinedkey:F8. : Resumes the execution.
If there are no additional breakpoints, then the execution just
continues and the debugger loses control.undefinedundefined
undefinedundefinedHere's what we can see after a click on it:

The execution has resumed, reached another breakpoint inside `say()` and paused there. Take a look at the "Call Stack" at the right. It has increased by one more call. We're inside `say()` now.undefinedundefined
undefinedundefinedkey:F9.undefinedundefinedRun the next
statement. If we click it now,
undefinedundefinedalert will be
shown.undefinedundefined
Clicking this again and again will step through all script statements one by one.
undefinedundefinedkey:F10.undefinedundefinedSimilar to the
previous "Step" command, but behaves differently if the next
statement is a function call. That is: not a built-in, like
undefinedundefinedalert, but a function of our
own.undefinedundefined
The "Step" command goes into it and pauses the execution at its first line, while "Step over" executes the nested function call invisibly, skipping the function internals.
undefinedundefinedThe execution is then paused immediately after that function.
undefinedundefinedThat's good if we're not interested to see what happens inside the function call.
undefinedundefinedkey:F11.undefinedundefinedThat's similar to "Step", but behaves differently in case of asynchronous function calls. If you're only starting to learn JavaScript, then you can ignore the difference, as we don't have asynchronous calls yet.
undefinedundefinedFor the
future, just note that "Step" command ignores async actions,
such as undefinedundefinedsetTimeout (scheduled
function call), that execute later. The "Step into" goes
into their code, waiting for them if necessary. See
undefinedundefinedDevTools
manual for more details.undefinedundefined
key:Shift+F11.undefinedundefined
smart header="Continue to here" Right click on a line of code opens the context menu with a great option called "Continue to here".
undefinedundefinedThat's handy when we want to move multiple steps forward to the line, but we're too lazy to set a breakpoint.
undefinedundefinedTo output something to console from our code,
there's undefinedundefinedconsole.log
function.undefinedundefined
For
instance, this outputs values from
undefinedundefined0 to
undefinedundefined4 to console:undefinedundefined
undefinedundefinedjs run // open console to see for (let i = 0; i < 5; i++) { console.log("value,", i); }undefinedundefined
Regular users don't see that output, it
is in the console. To see it, either open the Console panel of
developer tools or press undefinedundefinedkey:Esc
while in another panel: that opens the console at the
bottom.undefinedundefined
If we have enough logging in our code, then we can see what's going on from the records, without the debugger.
undefinedundefinedAs we can see,
there are three main ways to pause a script: 1. A breakpoint. 2.
The undefinedundefineddebugger statements. 3. An
error (if dev tools are open and the button
undefinedundefined is
"on").undefinedundefined
When paused, we can debug - examine variables and trace the code to see where the execution goes wrong.
undefinedundefinedThere are many more options in developer tools than covered here. The full manual is at undefinedundefinedhttps://developers.google.com/web/tools/chrome-devtools.undefinedundefined
undefinedundefinedThe information from this chapter is enough to begin debugging, but later, especially if you do a lot of browser stuff, please go there and look through more advanced capabilities of developer tools.
undefinedundefinedOh, and also you can click at various places of dev tools and just see what's showing up. That's probably the fastest route to learn dev tools. Don't forget about the right click and context menus!
undefinedundefinedOur code must be as clean and easy to read as possible.
undefinedundefinedThat is actually the art of programming - to take a complex task and code it in a way that is both correct and human-readable. A good code style greatly assists in that.
undefinedundefinedHere is a cheat sheet with some suggested rules (see below for more details):
undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Now let's discuss the rules and reasons for them in detail.
undefinedundefined
undefinedundefinedwarn header="There are no \"you must\" rules" Nothing is set in stone here. These are style preferences, not religious dogmas.undefinedundefined
In most JavaScript projects curly braces are written in "Egyptian" style with the opening brace on the same line as the corresponding keyword - not on a new line. There should also be a space before the opening bracket, like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedif (condition) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// do thisundefinedundefinedundefinedundefinedundefinedundefined// ...and thatundefinedundefinedundefinedundefinedundefinedundefined// ...and thatundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
A single-line construct, such as
undefinedundefinedif (condition) doSomething(), is
an important edge case. Should we use braces at
all?undefinedundefined
Here are the annotated variants so you can judge their readability for yourself:
undefinedundefinedjs if (n < 0) *!*{*/!*alert(`Power ${n} is not supported`);*!*}*/!*undefinedundefined
js if (n < 0) alert(`Power ${n} is not supported`);undefinedundefined
js if (n < 0) alert(`Power ${n} is not supported`);undefinedundefined
js if (n < 0) { alert(`Power ${n} is not supported`); }undefinedundefined
For a very
brief code, one line is allowed,
e.g. undefinedundefinedif (cond) return null. But a
code block (the last variant) is usually more
readable.undefinedundefined
No one likes to read a long horizontal line of code. It's best practice to split them.
undefinedundefinedFor example:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// backtick quotes ` allow to split the string into multiple linesundefinedundefinedundefinedundefinedundefinedundefinedlet str undefinedundefined=undefinedundefined`undefinedundefinedundefinedundefinedundefinedundefined ECMA International's TC39 is a group of JavaScript developers,undefinedundefinedundefinedundefinedundefinedundefined implementers, academics, and more, collaborating with the communityundefinedundefinedundefinedundefinedundefinedundefined to maintain and evolve the definition of JavaScript.undefinedundefinedundefinedundefinedundefinedundefined`undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
And, for
undefinedundefinedif statements:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedif (undefinedundefinedundefinedundefined id undefinedundefined===undefinedundefined123undefinedundefined&&undefinedundefinedundefinedundefined moonPhase undefinedundefined===undefinedundefined'Waning Gibbous'undefinedundefined&&undefinedundefinedundefinedundefined zodiacSign undefinedundefined===undefinedundefined'Libra'undefinedundefinedundefinedundefined) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedletTheSorceryBegin()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The maximum line length should be agreed upon at the team-level. It's usually 80 or 120 characters.
undefinedundefinedThere are two types of indents:
undefinedundefinedundefinedundefinedHorizontal indents: 2 or 4 spaces.undefinedundefined
undefinedundefinedA horizontal indentation is made using either 2 or 4 spaces
or the horizontal tab symbol (key
undefinedundefinedkey:Tab). Which one to choose
is an old holy war. Spaces are more common
nowadays.undefinedundefined
One advantage of spaces over tabs is that spaces allow more flexible configurations of indents than the tab symbol.
undefinedundefinedFor instance, we can align the parameters with the opening bracket, like this:
undefinedundefined
undefinedundefinedjs no-beautify show(parameters, aligned, // 5 spaces padding at the left one, after, another ) { // ... }undefinedundefined
undefinedundefinedVertical indents: empty lines for splitting code into logical blocks.undefinedundefined
undefinedundefinedEven a single function can often be divided into logical blocks. In the example below, the initialization of variables, the main loop and returning the result are split vertically:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedpow(xundefinedundefined, n) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet result undefinedundefined=undefinedundefined1undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined// <--undefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedlet i undefinedundefined=undefinedundefined0undefinedundefined; i undefinedundefined< nundefinedundefined; iundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefined result undefinedundefined*= xundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined// <--undefinedundefinedundefinedundefinedundefinedundefinedreturn resultundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Insert an extra newline where it helps to make the code more readable. There should not be more than nine lines of code without a vertical indentation.
undefinedundefinedA semicolon should be present after each statement, even if it could possibly be skipped.
undefinedundefinedThere are languages where a semicolon is truly optional and it is rarely used. In JavaScript, though, there are cases where a line break is not interpreted as a semicolon, leaving the code vulnerable to errors. See more about that in the chapter undefinedundefinedinfo:structure#semicolon.undefinedundefined
undefinedundefinedIf you're an experienced JavaScript programmer, you may choose a no-semicolon code style like undefinedundefinedStandardJS. Otherwise, it's best to use semicolons to avoid possible pitfalls. The majority of developers put semicolons.undefinedundefined
undefinedundefinedTry to avoid nesting code too many levels deep.
undefinedundefinedFor example, in the loop, it's
sometimes a good idea to use the undefinedundefinedundefinedundefinedcontinueundefinedundefined
directive to avoid extra nesting.undefinedundefined
For example, instead of adding a nested
undefinedundefinedif conditional like
this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedlet i undefinedundefined=undefinedundefined0undefinedundefined; i undefinedundefined<undefinedundefined10undefinedundefined; iundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (cond) undefinedundefined{undefinedundefinedundefinedundefined ... undefinedundefined// <- one more nesting levelundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
We can write:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedlet i undefinedundefined=undefinedundefined0undefinedundefined; i undefinedundefined<undefinedundefined10undefinedundefined; iundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (undefinedundefined!cond) undefinedundefined*!*undefinedundefinedcontinueundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined ... // <- no extra nesting levelundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
A similar thing can be done with
undefinedundefinedif/else and
undefinedundefinedreturn.undefinedundefined
For example, two constructs below are identical.
undefinedundefinedOption 1:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedpow(xundefinedundefined, n) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (n undefinedundefined<undefinedundefined0) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined"Negative 'n' not supported")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedelseundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet result undefinedundefined=undefinedundefined1undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedlet i undefinedundefined=undefinedundefined0undefinedundefined; i undefinedundefined< nundefinedundefined; iundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefined result undefinedundefined*= xundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedreturn resultundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Option 2:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedpow(xundefinedundefined, n) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (n undefinedundefined<undefinedundefined0) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined"Negative 'n' not supported")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet result undefinedundefined=undefinedundefined1undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedlet i undefinedundefined=undefinedundefined0undefinedundefined; i undefinedundefined< nundefinedundefined; iundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefined result undefinedundefined*= xundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedreturn resultundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The second one is more readable because
the "special case" of undefinedundefinedn < 0 is
handled early on. Once the check is done we can move on to the
"main" code flow without the need for additional
nesting.undefinedundefined
If you are writing several "helper" functions and the code that uses them, there are three ways to organize the functions.
undefinedundefinedDeclare the functions undefinedundefinedabove the code that uses them:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// *!*function declarations*/!*undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedcreateElement() undefinedundefined{undefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedsetHandler(elem) undefinedundefined{undefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedwalkAround() undefinedundefined{undefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// *!*the code which uses them*/!*undefinedundefinedundefinedundefinedundefinedundefinedlet elem undefinedundefined=undefinedundefinedcreateElement()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedsetHandler(elem)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedwalkAround()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Code first, then functions
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// *!*the code which uses the functions*/!*undefinedundefinedundefinedundefinedundefinedundefinedlet elem undefinedundefined=undefinedundefinedcreateElement()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedsetHandler(elem)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedwalkAround()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// --- *!*helper functions*/!* ---undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedcreateElement() undefinedundefined{undefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedsetHandler(elem) undefinedundefined{undefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedwalkAround() undefinedundefined{undefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Mixed: a function is declared where it's first used.
undefinedundefinedMost of time, the second variant is preferred.
undefinedundefinedThat's because when reading code, we first want to know undefinedundefinedwhat it does. If the code goes first, then it becomes clear from the start. Then, maybe we won't need to read the functions at all, especially if their names are descriptive of what they actually do.undefinedundefined
undefinedundefinedA style guide contains general rules about "how to write" code, e.g. which quotes to use, how many spaces to indent, the maximal line length, etc. A lot of minor things.
undefinedundefinedWhen all members of a team use the same style guide, the code looks uniform, regardless of which team member wrote it.
undefinedundefinedOf course, a team can always write their own style guide, but usually there's no need to. There are many existing guides to choose from.
undefinedundefinedSome popular choices:
undefinedundefinedIf you're a novice developer, start with the cheat sheet at the beginning of this chapter. Then you can browse other style guides to pick up more ideas and decide which one you like best.
undefinedundefinedLinters are tools that can automatically check the style of your code and make improving suggestions.
undefinedundefinedThe great thing about them is that style-checking can also find some bugs, like typos in variable or function names. Because of this feature, using a linter is recommended even if you don't want to stick to one particular "code style".
undefinedundefinedHere are some well-known linting tools:
undefinedundefinedAll of them can do the job. The author uses undefinedundefinedESLint.undefinedundefined
undefinedundefinedMost linters are integrated with many popular editors: just enable the plugin in the editor and configure the style.
undefinedundefinedFor instance, for ESLint you should do the following:
undefinedundefinednpm install -g eslint (npm is a
JavaScript package installer).undefinedundefined.eslintrc in the root of your
JavaScript project (in the folder that contains all your
files).undefinedundefinedHere's an example of
an undefinedundefined.eslintrc
file:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined"extends"undefinedundefined:undefinedundefined"eslint:recommended"undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefined"env"undefinedundefined:undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined"browser"undefinedundefined:undefinedundefinedtrueundefinedundefined,undefinedundefinedundefinedundefinedundefinedundefined"node"undefinedundefined:undefinedundefinedtrueundefinedundefined,undefinedundefinedundefinedundefinedundefinedundefined"es6"undefinedundefined:undefinedundefinedtrueundefinedundefinedundefinedundefinedundefinedundefined},undefinedundefinedundefinedundefinedundefinedundefined"rules"undefinedundefined:undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined"no-console"undefinedundefined:undefinedundefined0undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefined"indent"undefinedundefined:undefinedundefined2undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Here the directive
undefinedundefined"extends" denotes that the
configuration is based on the "eslint:recommended" set of
settings. After that, we specify our own.undefinedundefined
It is also possible to download style rule sets from the web and extend them instead. See undefinedundefinedhttp://eslint.org/docs/user-guide/getting-started for more details about installation.undefinedundefined
undefinedundefinedAlso certain IDEs have built-in linting, which is convenient but not as customizable as ESLint.
undefinedundefinedAll syntax rules described in this chapter (and in the style guides referenced) aim to increase the readability of your code. All of them are debatable.
undefinedundefinedWhen we think about writing "better" code, the questions we should ask ourselves are: "What makes the code more readable and easier to understand?" and "What can help us avoid errors?" These are the main things to keep in mind when choosing and debating code styles.
undefinedundefinedReading popular style guides will allow you to keep up to date with the latest ideas about code style trends and best practices.
undefinedundefinedAs we know from
the chapter undefinedundefinedinfo:structure, comments can be single-line:
starting with undefinedundefined// and multiline:
undefinedundefined/* ... */.undefinedundefined
We normally use them to describe how and why the code works.
undefinedundefinedAt first sight, commenting might be obvious, but novices in programming often use them wrongly.
undefinedundefinedNovices tend to use comments to explain "what is going on in the code". Like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// This code will do this thing (...) and that thing (...)undefinedundefinedundefinedundefinedundefinedundefined// ...and who knows what else...undefinedundefinedundefinedundefinedveryundefinedundefined;undefinedundefinedundefinedundefinedcomplexundefinedundefined;undefinedundefinedundefinedundefinedcodeundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
But in good code, the amount of such "explanatory" comments should be minimal. Seriously, the code should be easy to understand without them.
undefinedundefinedThere's a great rule about that: "if the code is so unclear that it requires a comment, then maybe it should be rewritten instead".
undefinedundefinedSometimes it's beneficial to replace a code piece with a function, like here:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedshowPrimes(n) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednextPrimeundefinedundefined:undefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedlet i undefinedundefined=undefinedundefined2undefinedundefined; i undefinedundefined< nundefinedundefined; iundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefined// check if i is a prime numberundefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedlet j undefinedundefined=undefinedundefined2undefinedundefined; j undefinedundefined< iundefinedundefined; jundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (i undefinedundefined% j undefinedundefined==undefinedundefined0) undefinedundefinedcontinue nextPrimeundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined alertundefinedundefined(undefinedundefinediundefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The better variant, with a factored out
function
undefinedundefinedisPrime:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedshowPrimes(n) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedlet i undefinedundefined=undefinedundefined2undefinedundefined; i undefinedundefined< nundefinedundefined; iundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedif (undefinedundefined!undefinedundefinedisPrime(i)) undefinedundefinedcontinueundefinedundefined;*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined alertundefinedundefined(undefinedundefinediundefinedundefined)undefinedundefined; undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunction isPrimeundefinedundefined(undefinedundefinednundefinedundefined)undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined for undefinedundefined(undefinedundefinedlet i = 2; i < n; iundefinedundefined++)undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined if undefinedundefined(undefinedundefinedn % i == 0undefinedundefined)undefinedundefined return false;undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined return true;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Now we can understand the code easily. The function itself becomes the comment. Such code is called undefinedundefinedself-descriptive.undefinedundefined
undefinedundefinedAnd if we have a long "code sheet" like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// here we add whiskeyundefinedundefinedundefinedundefinedundefinedundefinedfor(undefinedundefinedlet i undefinedundefined=undefinedundefined0undefinedundefined; i undefinedundefined<undefinedundefined10undefinedundefined; iundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet drop undefinedundefined=undefinedundefinedgetWhiskey()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedsmell(drop)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedadd(dropundefinedundefined, glass)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// here we add juiceundefinedundefinedundefinedundefinedundefinedundefinedfor(undefinedundefinedlet t undefinedundefined=undefinedundefined0undefinedundefined; t undefinedundefined<undefinedundefined3undefinedundefined; tundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet tomato undefinedundefined=undefinedundefinedgetTomato()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedexamine(tomato)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedlet juice undefinedundefined=undefinedundefinedpress(tomato)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedadd(juiceundefinedundefined, glass)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Then it might be a better variant to refactor it into functions like:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedaddWhiskey(glass)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedaddJuice(glass)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedaddWhiskey(container) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedfor(undefinedundefinedlet i undefinedundefined=undefinedundefined0undefinedundefined; i undefinedundefined<undefinedundefined10undefinedundefined; iundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet drop undefinedundefined=undefinedundefinedgetWhiskey()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined//...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedaddJuice(container) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedfor(undefinedundefinedlet t undefinedundefined=undefinedundefined0undefinedundefined; t undefinedundefined<undefinedundefined3undefinedundefined; tundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet tomato undefinedundefined=undefinedundefinedgetTomato()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined//...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Once again, functions themselves tell what's going on. There's nothing to comment. And also the code structure is better when split. It's clear what every function does, what it takes and what it returns.
undefinedundefinedIn reality, we can't totally avoid "explanatory" comments. There are complex algorithms. And there are smart "tweaks" for purposes of optimization. But generally we should try to keep the code simple and self-descriptive.
undefinedundefinedSo, explanatory comments are usually bad. Which comments are good?
undefinedundefinedFor instance:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined/**undefinedundefinedundefinedundefinedundefinedundefined * Returns x raised to the n-th power.undefinedundefinedundefinedundefinedundefinedundefined *undefinedundefinedundefinedundefinedundefinedundefined * undefinedundefined@paramundefinedundefinedundefinedundefined{number}undefinedundefined x The number to raise.undefinedundefinedundefinedundefinedundefinedundefined * undefinedundefined@paramundefinedundefinedundefinedundefined{number}undefinedundefined n The power, must be a natural number.undefinedundefinedundefinedundefinedundefinedundefined * undefinedundefined@returnundefinedundefined {number} x raised to the n-th power.undefinedundefinedundefinedundefinedundefinedundefined */undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedpow(xundefinedundefined, n) undefinedundefined{undefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Such comments allow us to understand the purpose of the function and use it the right way without looking in its code.
undefinedundefinedBy the way, many editors like undefinedundefinedWebStorm can understand them as well and use them to provide autocomplete and some automatic code-checking.undefinedundefined
undefinedundefinedAlso, there are tools like undefinedundefinedJSDoc 3 that can generate HTML-documentation from the comments. You can read more information about JSDoc at undefinedundefinedhttp://usejsdoc.org/.undefinedundefined
undefinedundefinedWhat's written is important. But what's undefinedundefinednot written may be even more important to understand what's going on. Why is the task solved exactly this way? The code gives no answer.undefinedundefined
undefinedundefinedIf there are many ways to solve the task, why this one? Especially when it's not the most obvious one.
undefinedundefinedWithout such comments the following situation is possible: 1. You (or your colleague) open the code written some time ago, and see that it's "suboptimal". 2. You think: "How stupid I was then, and how much smarter I'm now", and rewrite using the "more obvious and correct" variant. 3. …The urge to rewrite was good. But in the process you see that the "more obvious" solution is actually lacking. You even dimly remember why, because you already tried it long ago. You revert to the correct variant, but the time was wasted.
undefinedundefinedComments that explain the solution are very important. They help to continue development the right way.
undefinedundefinedAn important sign of a good developer is comments: their presence and even their absence.
undefinedundefinedGood comments allow us to maintain the code well, come back to it after a delay and use it more effectively.
undefinedundefinedundefinedundefinedComment this:undefinedundefined
undefinedundefinedundefinedundefinedAvoid comments:undefinedundefined
undefinedundefinedComments are also used for auto-documenting tools like JSDoc3: they read them and generate HTML-docs (or docs in another format).
undefinedundefined
undefinedundefinedquote author="Confucius (Analects)" Learning without thought is labor lost; thought without learning is perilous.undefinedundefined
Programmer ninjas of the past used these tricks to sharpen the mind of code maintainers.
undefinedundefinedCode review gurus look for them in test tasks.
undefinedundefinedNovice developers sometimes use them even better than programmer ninjas.
undefinedundefinedRead them carefully and find out who you are - a ninja, a novice, or maybe a code reviewer?
undefinedundefined
undefinedundefinedwarn header="Irony detected" Many try to follow ninja paths. Few succeed.undefinedundefined
Make the code as short as possible. Show how smart you are.
undefinedundefinedLet subtle language features guide you.
undefinedundefinedFor instance, take a look at this ternary operator
undefinedundefined'?':undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// taken from a well-known javascript libraryundefinedundefinedundefinedundefinedi undefinedundefined= i undefinedundefined? i undefinedundefined<undefinedundefined0undefinedundefined?undefinedundefinedMath.undefinedundefinedmax(undefinedundefined0undefinedundefined, len undefinedundefined+ i) : i : undefinedundefined0undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Cool, right? If you write like that, a
developer who comes across this line and tries to understand
what is the value of undefinedundefinedi is going
to have a merry time. Then come to you, seeking for an
answer.undefinedundefined
Tell them that shorter is always better. Initiate them into the paths of ninja.
undefinedundefined
undefinedundefinedquote author="Laozi (Tao Te Ching)" The Dao hides in wordlessness. Only the Dao is well begun and well completed.undefinedundefined
Another way to code shorter is to use
single-letter variable names everywhere. Like
undefinedundefineda,
undefinedundefinedb or
undefinedundefinedc.undefinedundefined
A short variable disappears in the code like
a real ninja in the forest. No one will be able to find it using
"search" of the editor. And even if someone does, they won't be
able to "decipher" what the name
undefinedundefineda or
undefinedundefinedb means.undefinedundefined
…But there's an exception. A real ninja will
never use undefinedundefinedi as the counter in a
undefinedundefined"for" loop. Anywhere, but not
here. Look around, there are many more exotic letters. For
instance, undefinedundefinedx or
undefinedundefinedy.undefinedundefined
An exotic variable as a loop counter is
especially cool if the loop body takes 1-2 pages (make it longer
if you can). Then if someone looks deep inside the loop, they
won't be able to quickly figure out that the variable named
undefinedundefinedx is the loop
counter.undefinedundefined
If the team rules forbid the use of one-letter and vague names - shorten them, make abbreviations.
undefinedundefinedLike this:
undefinedundefinedlist ->
undefinedundefinedlst.undefinedundefineduserAgent
-> undefinedundefinedua.undefinedundefined
browser ->
undefinedundefinedbrsr.undefinedundefinedOnly the one with truly good intuition will be able to understand such names. Try to shorten everything. Only a worthy person should be able to uphold the development of your code.
undefinedundefined
undefinedundefinedquote author="Laozi (Tao Te Ching)" The great square is cornerless<br> The great vessel is last complete,<br> The great note is rarified sound,<br> The great image has no form.undefinedundefined
While choosing a name try to use the most
abstract word. Like undefinedundefinedobj,
undefinedundefineddata,
undefinedundefinedvalue,
undefinedundefineditem,
undefinedundefinedelem and so on.undefinedundefined
undefinedundefinedThe ideal name for a variable is
undefinedundefineddata.undefinedundefined
Use it everywhere you can. Indeed, every variable holds
undefinedundefineddata, right?undefinedundefined
…But what to do if
undefinedundefineddata is already taken? Try
undefinedundefinedvalue, it's also universal.
After all, a variable eventually gets a
undefinedundefinedvalue.undefinedundefined
undefinedundefinedName a variable
by its type: undefinedundefinedstr,
undefinedundefinednum…undefinedundefinedundefinedundefined
Give them a try. A young initiate may wonder - are such names really useful for a ninja? Indeed, they are!
undefinedundefinedSure, the variable name still means something. It says what's inside the variable: a string, a number or something else. But when an outsider tries to understand the code, they'll be surprised to see that there's actually no information at all! And will ultimately fail to alter your well-thought code.
undefinedundefinedThe value type is easy to find out by debugging. But what's the meaning of the variable? Which string/number does it store?
undefinedundefinedThere's just no way to figure out without a good meditation!
undefinedundefinedundefinedundefined…But what if
there are no more such names? Just add a number:
undefinedundefineddata1, item2, elem5…undefinedundefined
Only a truly attentive programmer should be able to understand your code. But how to check that?
undefinedundefinedundefinedundefinedOne of the ways -
use similar variable names, like
undefinedundefineddate and
undefinedundefineddata.undefinedundefinedundefinedundefined
Mix them where you can.
undefinedundefinedA quick read of such code becomes impossible. And when there's a typo… Ummm… We're stuck for long, time to drink tea.
undefinedundefined
undefinedundefinedquote author="Laozi (Tao Te Ching)" The Tao that can be told is not the eternal Tao. The name that can be named is not the eternal name.undefinedundefined
Using undefinedundefinedsimilar names for undefinedundefinedsame things makes life more interesting and shows your creativity to the public.undefinedundefined
undefinedundefinedFor instance,
consider function prefixes. If a function shows a message on the
screen - start it with undefinedundefineddisplay…,
like undefinedundefineddisplayMessage. And then if
another function shows on the screen something else, like a user
name, start it with undefinedundefinedshow… (like
undefinedundefinedshowName).undefinedundefined
Insinuate that there's a subtle difference between such functions, while there is none.
undefinedundefinedMake a pact with fellow ninjas of the team:
if John starts "showing" functions with
undefinedundefineddisplay... in his code, then
Peter could use undefinedundefinedrender.., and Ann
- undefinedundefinedpaint.... Note how much more
interesting and diverse the code became.undefinedundefined
…And now the hat trick!
undefinedundefinedFor two functions with important differences - use the same prefix!
undefinedundefinedFor instance, the function
undefinedundefinedprintPage(page) will use a
printer. And the function
undefinedundefinedprintText(text) will put the text
on-screen. Let an unfamiliar reader think well over similarly
named function undefinedundefinedprintMessage:
"Where does it put the message? To a printer or on the screen?".
To make it really shine,
undefinedundefinedprintMessage(message) should
output it in the new window!undefinedundefined
undefinedundefinedquote author="Laozi (Tao Te Ching)" Once the whole is divided, the parts<br> need names.<br> There are already enough names.<br> One must know when to stop.undefinedundefined
Add a new variable only when absolutely necessary.
undefinedundefinedInstead, reuse existing names. Just write new values into them.
undefinedundefinedIn a function try to use only variables passed as parameters.
undefinedundefinedThat would make it really hard to identify what's exactly in the variable undefinedundefinednow. And also where it comes from. The purpose is to develop the intuition and memory of a person reading the code. A person with weak intuition would have to analyze the code line-by-line and track the changes through every code branch.undefinedundefined
undefinedundefinedundefinedundefinedAn advanced variant of the approach is to covertly (!) replace the value with something alike in the middle of a loop or a function.undefinedundefined
undefinedundefinedFor instance:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedninjaFunction(elem) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// 20 lines of code working with elemundefinedundefinedundefinedundefinedundefinedundefined elem undefinedundefined=undefinedundefinedclone(elem)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// 20 more lines, now working with the clone of the elem!undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
A fellow programmer who wants to work
with undefinedundefinedelem in the second half of
the function will be surprised… Only during the debugging, after
examining the code they will find out that they're working with
a clone!undefinedundefined
Seen in code regularly. Deadly effective even against an experienced ninja.
undefinedundefinedPut underscores
undefinedundefined_ and
undefinedundefined__ before variable names. Like
undefinedundefined_name or
undefinedundefined__value. It would be great if
only you knew their meaning. Or, better, add them just for fun,
without particular meaning at all. Or different meanings in
different places.undefinedundefined
You kill two rabbits with one shot. First, the code becomes longer and less readable, and the second, a fellow developer may spend a long time trying to figure out what the underscores mean.
undefinedundefinedA smart ninja puts underscores at one spot of code and evades them at other places. That makes the code even more fragile and increases the probability of future errors.
undefinedundefinedLet everyone see how magnificent your
entities are! Names like
undefinedundefinedsuperElement,
undefinedundefinedmegaFrame and
undefinedundefinedniceItem will definitely
enlighten a reader.undefinedundefined
Indeed, from one hand, something is written:
undefinedundefinedsuper..,
undefinedundefinedmega..,
undefinedundefinednice.. But from the other hand -
that brings no details. A reader may decide to look for a hidden
meaning and meditate for an hour or two of their paid working
time.undefinedundefined
undefinedundefinedquote author="Guan Yin Zi" When in the light, can't see anything in the darkness.<br> When in the darkness, can see everything in the light.undefinedundefined
Use same names for variables inside and outside a function. As simple. No efforts to invent new names.
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedletundefinedundefined*!*userundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefined = authenticateUserundefinedundefined()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunction renderundefinedundefined()undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined let undefinedundefined*undefinedundefined!undefinedundefined*undefinedundefineduserundefinedundefined*undefinedundefined/undefinedundefined!*undefinedundefined=undefinedundefinedanotherValue()undefinedundefined;undefinedundefinedundefinedundefined ...undefinedundefined ...undefinedundefinedmanyundefinedundefinedlines...undefinedundefinedundefinedundefined ...undefinedundefined ... undefinedundefined// <-- a programmer wants to work with user here and...undefinedundefinedundefinedundefined ...undefinedundefined}undefinedundefinedundefinedundefined
undefinedundefined
A programmer who jumps inside the
undefinedundefinedrender will probably fail to
notice that there's a local undefinedundefineduser
shadowing the outer one.undefinedundefined
Then they'll try to work with
undefinedundefineduser assuming that it's the
external variable, the result of
undefinedundefinedauthenticateUser()… The trap is
sprung! Hello, debugger…undefinedundefined
There are functions that look like they don't
change anything. Like undefinedundefinedisReady(),
undefinedundefinedcheckPermission(),
undefinedundefinedfindTags()… They are assumed to
carry out calculations, find and return the data, without
changing anything outside of them. In other words, without
"side-effects".undefinedundefined
undefinedundefinedA really beautiful trick is to add a "useful" action to them, besides the main task.undefinedundefined
undefinedundefinedAn
expression of dazed surprise on the face of your colleague when
they see a function named undefinedundefinedis..,
undefinedundefinedcheck.. or
undefinedundefinedfind... changing something - will
definitely broaden your boundaries of reason.undefinedundefined
undefinedundefinedAnother way to surprise is to return a non-standard result.undefinedundefined
undefinedundefined
Show your original thinking! Let the call of
undefinedundefinedcheckPermission return not
undefinedundefinedtrue/false, but a complex object
with the results of the check.undefinedundefined
Those developers who try to write
undefinedundefinedif (checkPermission(..)), will
wonder why it doesn't work. Tell them: "Read the docs!". And
give this article.undefinedundefined
undefinedundefinedquote author="Laozi (Tao Te Ching)" The great Tao flows everywhere,<br> both to the left and to the right.undefinedundefined
Don't limit the function by what's written in its name. Be broader.
undefinedundefinedFor
instance, a function
undefinedundefinedvalidateEmail(email) could
(besides checking the email for correctness) show an error
message and ask to re-enter the email.undefinedundefined
Additional actions should not be obvious from the function name. A true ninja coder will make them not obvious from the code as well.
undefinedundefinedundefinedundefinedJoining several actions into one protects your code from reuse.undefinedundefined
undefinedundefinedImagine, another developer wants only to
check the email, and not output any message. Your function
undefinedundefinedvalidateEmail(email) that does
both will not suit them. So they won't break your meditation by
asking anything about it.undefinedundefined
All "pieces of advice" above are from the real code… Sometimes, written by experienced developers. Maybe even more experienced than you are ;)
undefinedundefinedAutomated testing will be used in further tasks, and it's also widely used in real projects.
undefinedundefinedWhen we write a function, we can usually imagine what it should do: which parameters give which results.
undefinedundefinedDuring development, we can check the function by running it and comparing the outcome with the expected one. For instance, we can do it in the console.
undefinedundefinedIf something is wrong - then we fix the code, run again, check the result - and so on till it works.
undefinedundefinedBut such manual "re-runs" are imperfect.
undefinedundefinedundefinedundefinedWhen testing a code by manual re-runs, it's easy to miss something.undefinedundefined
undefinedundefined
For instance, we're creating a function
undefinedundefinedf. Wrote some code, testing:
undefinedundefinedf(1) works, but
undefinedundefinedf(2) doesn't work. We fix the
code and now undefinedundefinedf(2) works. Looks
complete? But we forgot to re-test
undefinedundefinedf(1). That may lead to an
error.undefinedundefined
That's very typical. When we develop something, we keep a lot of possible use cases in mind. But it's hard to expect a programmer to check all of them manually after every change. So it becomes easy to fix one thing and break another one.
undefinedundefinedundefinedundefinedAutomated testing means that tests are written separately, in addition to the code. They run our functions in various ways and compare results with the expected.undefinedundefined
undefinedundefinedLet's start with a technique named undefinedundefinedBehavior Driven Development or, in short, BDD.undefinedundefined
undefinedundefinedundefinedundefinedBDD is three things in one: tests AND documentation AND examples.undefinedundefined
undefinedundefinedTo understand BDD, we'll examine a practical case of development.
undefinedundefinedLet's say we want to make a function
undefinedundefinedpow(x, n) that raises
undefinedundefinedx to an integer power
undefinedundefinedn. We assume that
undefinedundefinedn≥0.undefinedundefined
That task is just an example: there's the
undefinedundefined** operator in JavaScript that
can do that, but here we concentrate on the development flow
that can be applied to more complex tasks as
well.undefinedundefined
Before creating
the code of undefinedundefinedpow, we can imagine
what the function should do and describe it.undefinedundefined
Such description is called a undefinedundefinedspecification or, in short, a spec, and contains descriptions of use cases together with tests for them, like this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefineddescribe(undefinedundefined"pow"undefinedundefined,undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedit(undefinedundefined"raises to n-th power"undefinedundefined,undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedassert.undefinedundefinedequal(undefinedundefinedpow(undefinedundefined2undefinedundefined,undefinedundefined3)undefinedundefined,undefinedundefined8)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
A spec has three main building blocks that you can see above:
undefinedundefineddescribe("title", function() { ... })undefinedundefined
pow. Used to group "workers" -
the undefinedundefinedit blocks.
undefinedundefinedit("use case description", function() { ... })undefinedundefined
it we undefinedundefinedin
a human-readable way describe the particular use case,
and the second argument is a function that tests it.
undefinedundefinedassert.equal(value1, value2)undefinedundefined
The code inside
undefinedundefinedit block, if the
implementation is correct, should execute without
errors.undefinedundefined
Functions
undefinedundefinedassert.* are used to check
whether undefinedundefinedpow works as
expected. Right here we're using one of them -
undefinedundefinedassert.equal, it compares
arguments and yields an error if they are not equal. Here it
checks that the result of
undefinedundefinedpow(2, 3) equals
undefinedundefined8. There are other types of
comparisons and checks, that we'll add
later.undefinedundefined
The specification
can be executed, and it will run the test specified in
undefinedundefinedit block. We'll see that
later.undefinedundefined
The flow of development usually looks like this:
undefinedundefinedSo, the development is undefinedundefinediterative. We write the spec, implement it, make sure tests pass, then write more tests, make sure they work etc. At the end we have both a working implementation and tests for it.undefinedundefined
undefinedundefinedLet's see this development flow in our practical case.
undefinedundefinedThe first step is
already complete: we have an initial spec for
undefinedundefinedpow. Now, before making the
implementation, let's use few JavaScript libraries to run the
tests, just to see that they are working (they will all
fail).undefinedundefined
Here in the tutorial we'll be using the following JavaScript libraries for tests:
undefinedundefineddescribe and
undefinedundefinedit and the main function that
runs tests.undefinedundefinedassert.equal.undefinedundefined
These libraries are suitable for both in-browser and server-side testing. Here we'll consider the browser variant.
undefinedundefinedThe full HTML page with
these frameworks and undefinedundefinedpow
spec:undefinedundefined
undefinedundefinedhtml src="index.html"undefinedundefined
The page can be divided into five parts:
undefinedundefined<head> - add third-party
libraries and styles for tests.undefinedundefined<script> with the
function to test, in our case - with the code for
undefinedundefinedpow.undefinedundefinedtest.js that has
undefinedundefineddescribe("pow", ...) from
above.undefinedundefined<div id="mocha">
will be used by Mocha to output results.undefinedundefined
mocha.run().undefinedundefined
The result:
undefinedundefined[iframe height=250 src="pow-1" border=1 edit]
undefinedundefinedAs of now, the test fails, there's an
error. That's logical: we have an empty function code in
undefinedundefinedpow, so
undefinedundefinedpow(2,3) returns
undefinedundefinedundefined instead of
undefinedundefined8.undefinedundefined
For the future, let's note that there are more high-level test-runners, like undefinedundefinedkarma and others, that make it easy to autorun many different tests.undefinedundefined
undefinedundefinedLet's make a simple implementation of
undefinedundefinedpow, for tests to
pass:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedpow(xundefinedundefined, n) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefined8undefinedundefined;undefinedundefined// :) we cheat!undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Wow, now it works!
undefinedundefined[iframe height=250 src="pow-min" border=1 edit]
undefinedundefinedWhat we've done is definitely
a cheat. The function does not work: an attempt to calculate
undefinedundefinedpow(3,4) would give an incorrect
result, but tests pass.undefinedundefined
…But the situation is quite typical, it happens in practice. Tests pass, but the function works wrong. Our spec is imperfect. We need to add more use cases to it.
undefinedundefined
Let's add one more test to check that
undefinedundefinedpow(3, 4) = 81.undefinedundefined
We can select one of two ways to organize the test here:
undefinedundefinedThe first variant -
add one more undefinedundefinedassert into the
same undefinedundefinedit:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefineddescribe(undefinedundefined"pow"undefinedundefined,undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedit(undefinedundefined"raises to n-th power"undefinedundefined,undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedassert.undefinedundefinedequal(undefinedundefinedpow(undefinedundefined2undefinedundefined,undefinedundefined3)undefinedundefined,undefinedundefined8)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedassert.undefinedundefinedequal(undefinedundefinedpow(undefinedundefined3undefinedundefined,undefinedundefined4)undefinedundefined,undefinedundefined81)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The second - make two tests:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefineddescribe(undefinedundefined"pow"undefinedundefined,undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedit(undefinedundefined"2 raised to power 3 is 8"undefinedundefined,undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedassert.undefinedundefinedequal(undefinedundefinedpow(undefinedundefined2undefinedundefined,undefinedundefined3)undefinedundefined,undefinedundefined8)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedit(undefinedundefined"3 raised to power 4 is 81"undefinedundefined,undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedassert.undefinedundefinedequal(undefinedundefinedpow(undefinedundefined3undefinedundefined,undefinedundefined4)undefinedundefined,undefinedundefined81)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The principal difference is that when
undefinedundefinedassert triggers an error, the
undefinedundefinedit block immediately terminates.
So, in the first variant if the first
undefinedundefinedassert fails, then we'll never
see the result of the second
undefinedundefinedassert.undefinedundefined
Making tests separate is useful to get more information about what's going on, so the second variant is better.
undefinedundefinedAnd besides that, there's one more rule that's good to follow.
undefinedundefinedundefinedundefinedOne test checks one thing.undefinedundefined
undefinedundefinedIf we look at the test and see two independent checks in it, it's better to split it into two simpler ones.
undefinedundefinedSo let's continue with the second variant.
undefinedundefinedThe result:
undefinedundefined[iframe height=250 src="pow-2" edit border="1"]
undefinedundefined
As we could expect, the second test failed. Sure, our function
always returns undefinedundefined8, while the
undefinedundefinedassert expects
undefinedundefined81.undefinedundefined
Let's write something more real for tests to pass:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedpow(xundefinedundefined, n) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet result undefinedundefined=undefinedundefined1undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedlet i undefinedundefined=undefinedundefined0undefinedundefined; i undefinedundefined< nundefinedundefined; iundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefined result undefinedundefined*= xundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedreturn resultundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
To be sure that the function works
well, let's test it for more values. Instead of writing
undefinedundefinedit blocks manually, we can
generate them in
undefinedundefinedfor:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefineddescribe(undefinedundefined"pow"undefinedundefined,undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedmakeTest(x) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet expected undefinedundefined= x undefinedundefined* x undefinedundefined* xundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedit(undefinedundefined`undefinedundefined${xundefinedundefined}undefinedundefined in the power 3 is undefinedundefined${expectedundefinedundefined}undefinedundefined`undefinedundefined,undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedassert.undefinedundefinedequal(undefinedundefinedpow(xundefinedundefined,undefinedundefined3)undefinedundefined, expected)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedlet x undefinedundefined=undefinedundefined1undefinedundefined; x undefinedundefined<=undefinedundefined5undefinedundefined; xundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedmakeTest(x)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The result:
undefinedundefined[iframe height=250 src="pow-3" edit border="1"]
undefinedundefinedWe're going to add even more tests. But
before that let's note that the helper function
undefinedundefinedmakeTest and
undefinedundefinedfor should be grouped together.
We won't need undefinedundefinedmakeTest in other
tests, it's needed only in undefinedundefinedfor:
their common task is to check how
undefinedundefinedpow raises into the given
power.undefinedundefined
Grouping is
done with a nested
undefinedundefineddescribe:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefineddescribe(undefinedundefined"pow"undefinedundefined,undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefineddescribe(undefinedundefined"raises x to power 3"undefinedundefined,undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined function makeTestundefinedundefined(undefinedundefinedxundefinedundefined)undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined let expected = x undefinedundefined*undefinedundefined x undefinedundefined*undefinedundefined x;undefinedundefinedundefinedundefinedundefinedundefined itundefinedundefined(undefinedundefined`${x} in the power 3 is ${expected}`, functionundefinedundefined()undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined assert.equalundefinedundefined(undefinedundefinedpowundefinedundefined(undefinedundefinedx, 3undefinedundefined)undefinedundefined, expectedundefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined for undefinedundefined(undefinedundefinedlet x = 1; x <= 5; xundefinedundefined++)undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined makeTestundefinedundefined(undefinedundefinedxundefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/undefinedundefined!*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// ... more tests to follow here, both describe and it can be addedundefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The nested
undefinedundefineddescribe defines a new "subgroup"
of tests. In the output we can see the titled
indentation:undefinedundefined
[iframe height=250 src="pow-4" edit border="1"]
undefinedundefined
In the future we can add more undefinedundefinedit
and undefinedundefineddescribe on the top level
with helper functions of their own, they won't see
undefinedundefinedmakeTest.undefinedundefined
undefinedundefinedsmart header="before/afterundefinedundefinedandbeforeEach/afterEachundefinedundefined" We can setupbefore/afterundefinedundefinedfunctions that execute before/after running tests, and alsobeforeEach/afterEachundefinedundefinedfunctions that execute before/after *every*it`.undefinedundefined
For instance:
undefinedundefinedno-beautify describe("test", function() {
undefinedundefinedbefore(() => alert("Testing started - before all tests")); after(() => alert("Testing finished - after all tests"));
undefinedundefinedbeforeEach(() => alert("Before a test - enter a test")); afterEach(() => alert("After a test - exit a test"));
undefinedundefinedit(‘test 1', () => alert(1)); it(‘test 2', () => alert(2));
undefinedundefined});
undefinedundefinedundefinedundefined
The running sequence will be:
undefinedundefinedundefinedundefinedTesting started - before all tests (before) Before a test - enter a test (beforeEach) 1 After a test - exit a test (afterEach) Before a test - enter a test (beforeEach) 2 After a test - exit a test (afterEach) Testing finished - after all tests (after)
undefinedundefinedundefinedundefined
[edit src="beforeafter" title="Open the example in the sandbox."]
Usually, `beforeEach/afterEach` and `before/after` are used to perform initialization, zero out counters or do something else between the tests (or test groups).undefinedundefined
undefinedundefinedThe basic functionality of
undefinedundefinedpow is complete. The first
iteration of the development is done. When we're done
celebrating and drinking champagne - let's go on and improve
it.undefinedundefined
As it was said,
the function undefinedundefinedpow(x, n) is meant
to work with positive integer values
undefinedundefinedn.undefinedundefined
To indicate a mathematical error, JavaScript
functions usually return undefinedundefinedNaN.
Let's do the same for invalid values of
undefinedundefinedn.undefinedundefined
Let's first add the behavior to the spec(!):
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefineddescribe(undefinedundefined"pow"undefinedundefined,undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedit(undefinedundefined"for negative n the result is NaN"undefinedundefined,undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedassert.undefinedundefinedisNaN(undefinedundefinedpow(undefinedundefined2undefinedundefined,undefinedundefined-1))undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined itundefinedundefined(undefinedundefined"for non-integer n the result is NaN", functionundefinedundefined()undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined assert.isNaNundefinedundefined(undefinedundefinedpowundefinedundefined(undefinedundefined2, 1.5undefinedundefined))undefinedundefined; undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/undefinedundefined!*undefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The result with new tests:
undefinedundefined[iframe height=530 src="pow-nan" edit border="1"]
undefinedundefinedThe newly added tests fail, because our implementation does not support them. That's how BDD is done: first we write failing tests, and then make an implementation for them.
undefinedundefined
``undefinedundefinedsmart header="Other assertions" Please note the assertionassert.isNaNundefinedundefined: it checks forNaN`.undefinedundefined
There are other assertions in undefinedundefinedChai as well, for instance:undefinedundefined
undefinedundefinedassert.equal(value1, value2) -
checks the equality
undefinedundefinedvalue1 == value2.undefinedundefined
assert.strictEqual(value1, value2)
- checks the strict equality
undefinedundefinedvalue1 === value2.undefinedundefined
assert.notEqual,
undefinedundefinedassert.notStrictEqual - inverse
checks to the ones above.undefinedundefinedassert.isTrue(value) - checks
that
undefinedundefinedvalue === trueundefinedundefined
assert.isFalse(value) - checks
that
undefinedundefinedvalue === falseundefinedundefined
So we should add a couple of lines to
undefinedundefinedpow:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedpow(xundefinedundefined, n) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedif (n undefinedundefined<undefinedundefined0) undefinedundefinedreturnundefinedundefinedNaNundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedif (undefinedundefinedMath.undefinedundefinedround(n) undefinedundefined!= n) undefinedundefinedreturnundefinedundefinedNaNundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined let result = 1;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined for undefinedundefined(undefinedundefinedlet i = 0; i < n; iundefinedundefined++)undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined result undefinedundefined*undefinedundefined= x;undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined return result;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Now it works, all tests pass:
undefinedundefined[iframe height=300 src="pow-full" edit border="1"]
undefinedundefined[edit src="pow-full" title="Open the full final example in the sandbox."]
undefinedundefinedIn BDD, the spec goes first, followed by implementation. At the end we have both the spec and the code.
undefinedundefinedThe spec can be used in three ways:
undefinedundefineddescribe and
undefinedundefinedit tell what the function
does.undefinedundefinedWith the spec, we can safely improve, change, even rewrite the function from scratch and make sure it still works right.
undefinedundefinedThat's especially important in large projects when a function is used in many places. When we change such a function, there's just no way to manually check if every place that uses it still works right.
undefinedundefinedWithout tests, people have two ways:
undefinedundefinedundefinedundefinedAutomatic testing helps to avoid these problems!undefinedundefined
undefinedundefinedIf the project is covered with tests, there's just no such problem. After any changes, we can run tests and see a lot of checks made in a matter of seconds.
undefinedundefinedundefinedundefinedBesides, a well-tested code has better architecture.undefinedundefined
undefinedundefinedNaturally, that's because auto-tested code is easier to modify and improve. But there's also another reason.
undefinedundefinedTo write tests, the code should be organized in such a way that every function has a clearly described task, well-defined input and output. That means a good architecture from the beginning.
undefinedundefinedIn real life that's sometimes not that easy. Sometimes it's difficult to write a spec before the actual code, because it's not yet clear how it should behave. But in general writing tests makes development faster and more stable.
undefinedundefinedLater in the tutorial you will meet many tasks with tests baked-in. So you'll see more practical examples.
undefinedundefinedWriting tests requires good JavaScript knowledge. But we're just starting to learn it. So, to settle down everything, as of now you're not required to write tests, but you should already be able to read them even if they are a little bit more complex than in this chapter.
undefinedundefinedThe JavaScript language steadily evolves. New proposals to the language appear regularly, they are analyzed and, if considered worthy, are appended to the list at undefinedundefinedhttps://tc39.github.io/ecma262/ and then progress to the undefinedundefinedspecification.undefinedundefined
undefinedundefinedTeams behind JavaScript engines have their own ideas about what to implement first. They may decide to implement proposals that are in draft and postpone things that are already in the spec, because they are less interesting or just harder to do.
undefinedundefinedSo it's quite common for an engine to implement only the part of the standard.
undefinedundefinedA good page to see the current state of support for language features is undefinedundefinedhttps://kangax.github.io/compat-table/es6/ (it's big, we have a lot to study yet).undefinedundefined
undefinedundefinedAs programmers, we'd like to use most recent features. The more good stuff - the better!
undefinedundefinedOn the other hand, how to make our modern code work on older engines that don't understand recent features yet?
undefinedundefinedThere are two tools for that:
undefinedundefinedHere, in this chapter, our purpose is to get the gist of how they work, and their place in web development.
undefinedundefinedA undefinedundefinedtranspiler is a special piece of software that can parse ("read and understand") modern code, and rewrite it using older syntax constructs, so that the result would be the same.undefinedundefined
undefinedundefinedE.g. JavaScript
before year 2020 didn't have the "nullish coalescing operator"
undefinedundefined??. So, if a visitor uses an
outdated browser, it may fail to understand the code like
undefinedundefinedheight = height ?? 100.undefinedundefined
A transpiler would analyze our code and
rewrite undefinedundefinedheight ?? 100 into
undefinedundefined(height !== undefined && height !== null) ? height : 100.undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// before running the transpilerundefinedundefinedundefinedundefinedheight undefinedundefined= height undefinedundefined??undefinedundefined100undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// after running the transpilerundefinedundefinedundefinedundefinedheight undefinedundefined= (height undefinedundefined!==undefinedundefinedundefinedundefinedundefined&& height undefinedundefined!==undefinedundefinednull) undefinedundefined? height : undefinedundefined100undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Now the rewritten code is suitable for older JavaScript engines.
undefinedundefinedUsually, a developer runs the transpiler on their own computer, and then deploys the transpiled code to the server.
undefinedundefinedSpeaking of names, undefinedundefinedBabel is one of the most prominent transpilers out there.undefinedundefined
undefinedundefinedModern project build systems, such as undefinedundefinedwebpack, provide means to run transpiler automatically on every code change, so it's very easy to integrate into development process.undefinedundefined
undefinedundefinedNew language features may include not only syntax constructs and operators, but also built-in functions.
undefinedundefinedFor example,
undefinedundefinedMath.trunc(n) is a function that
"cuts off" the decimal part of a number, e.g
undefinedundefinedMath.trunc(1.23) returns
undefinedundefined1.undefinedundefined
In some (very outdated) JavaScript engines,
there's no undefinedundefinedMath.trunc, so such
code will fail.undefinedundefined
As we're talking about new functions, not syntax changes, there's no need to transpile anything here. We just need to declare the missing function.
undefinedundefinedA script that updates/adds new functions is called "polyfill". It "fills in" the gap and adds missing implementations.
undefinedundefinedFor this particular case, the polyfill for
undefinedundefinedMath.trunc is a script that
implements it, like this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedif (undefinedundefined!undefinedundefinedMath.undefinedundefinedtrunc) undefinedundefined{undefinedundefined// if no such functionundefinedundefinedundefinedundefinedundefinedundefined// implement itundefinedundefinedundefinedundefinedundefinedundefinedMath.undefinedundefinedtruncundefinedundefined=undefinedundefinedfunction(number) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// Math.ceil and Math.floor exist even in ancient JavaScript enginesundefinedundefinedundefinedundefinedundefinedundefined// they are covered later in the tutorialundefinedundefinedundefinedundefinedundefinedundefinedreturn number undefinedundefined<undefinedundefined0undefinedundefined?undefinedundefinedMath.undefinedundefinedceil(number) : undefinedundefinedMath.undefinedundefinedfloor(number)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
JavaScript is a highly dynamic language, scripts may add/modify any functions, even including built-in ones.
undefinedundefinedTwo interesting libraries of polyfills are: - undefinedundefinedcore js that supports a lot, allows to include only needed features. - undefinedundefinedpolyfill.io service that provides a script with polyfills, depending on the features and user's browser.undefinedundefined
undefinedundefinedIn this chapter we'd like to motivate you to study modern and even "bleeding-edge" language features, even if they aren't yet well-supported by JavaScript engines.
undefinedundefinedJust don't forget to use transpiler (if using modern syntax or operators) and polyfills (to add functions that may be missing). And they'll ensure that the code works.
undefinedundefinedFor example, later when you're familiar with JavaScript, you can setup a code build system based on undefinedundefinedwebpack with undefinedundefinedbabel-loader plugin.undefinedundefined
undefinedundefinedGood resources that show the current state of support for various features: - undefinedundefinedhttps://kangax.github.io/compat-table/es6/ - for pure JavaScript. - undefinedundefinedhttps://caniuse.com/ - for browser-related functions.undefinedundefined
undefinedundefinedP.S. Google Chrome is usually the most up-to-date with language features, try it if a tutorial demo fails. Most tutorial demos work with any modern browser though.
undefinedundefinedAs we know from the chapter undefinedundefinedinfo:types, there are eight data types in JavaScript. Seven of them are called "primitive", because their values contain only a single thing (be it a string or a number or whatever).undefinedundefined
undefinedundefinedIn contrast, objects are used to store keyed collections of various data and more complex entities. In JavaScript, objects penetrate almost every aspect of the language. So we must understand them first before going in-depth anywhere else.
undefinedundefinedAn object can be created with figure brackets
undefinedundefined{…} with an optional list of
undefinedundefinedproperties. A property is a "key:
value" pair, where undefinedundefinedkey is a
string (also called a "property name"), and
undefinedundefinedvalue can be
anything.undefinedundefined
We can imagine an object as a cabinet with signed files. Every piece of data is stored in its file by the key. It's easy to find a file by its name or add/remove a file.
undefinedundefined
undefinedundefinedundefinedundefined
An empty object ("empty cabinet") can be created using one of two syntaxes:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefinednewundefinedundefinedObject()undefinedundefined;undefinedundefined// "object constructor" syntaxundefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{};undefinedundefined// "object literal" syntaxundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedundefinedundefined
Usually, the figure brackets
undefinedundefined{...} are used. That declaration
is called an undefinedundefinedobject
literal.undefinedundefined
We can immediately put some properties into
undefinedundefined{...} as "key: value"
pairs:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{undefinedundefined// an objectundefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefined,undefinedundefined// by key "name" store value "John"undefinedundefinedundefinedundefinedundefinedundefinedageundefinedundefined:undefinedundefined30undefinedundefined// by key "age" store value 30undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
A property has a key (also known as
"name" or "identifier") before the colon
undefinedundefined":" and a value to the right of
it.undefinedundefined
In the
undefinedundefineduser object, there are two
properties:undefinedundefined
"name" and the value
undefinedundefined"John".undefinedundefined"age" and the value
undefinedundefined30.undefinedundefinedThe resulting
undefinedundefineduser object can be imagined as a
cabinet with two signed files labeled "name" and
"age".undefinedundefined
We can add, remove and read files from it any time.
undefinedundefinedProperty values are accessible using the dot notation:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// get property values of the object:undefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefineduser.undefinedundefinedname )undefinedundefined;undefinedundefined// Johnundefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefineduser.undefinedundefinedage )undefinedundefined;undefinedundefined// 30undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The value can be of any type. Let's add a boolean one:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefineduser.undefinedundefinedisAdminundefinedundefined=undefinedundefinedtrueundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
To remove a property, we can use
undefinedundefineddelete
operator:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefineddeleteundefinedundefineduser.undefinedundefinedageundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
We can also use multiword property names, but then they must be quoted:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedageundefinedundefined:undefinedundefined30undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefined"likes birds"undefinedundefined:undefinedundefinedtrueundefinedundefined// multiword property name must be quotedundefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedundefinedundefined
The last property in the list may end with a comma:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedageundefinedundefined:undefinedundefined30undefinedundefined*!*,*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That is called a "trailing" or "hanging" comma. Makes it easier to add/remove/move around properties, because all lines become alike.
undefinedundefinedFor multiword properties, the dot access doesn't work:
undefinedundefined
undefinedundefinedjs run // this would give a syntax error user.likes birds = trueundefinedundefined
JavaScript doesn't understand that. It
thinks that we address
undefinedundefineduser.likes, and then gives a
syntax error when comes across unexpected
undefinedundefinedbirds.undefinedundefined
The dot requires the key to be a valid
variable identifier. That implies: contains no spaces, doesn't
start with a digit and doesn't include special characters
(undefinedundefined$ and
undefinedundefined_ are allowed).undefinedundefined
There's an alternative "square bracket notation" that works with any string:
undefinedundefinedrun let user = {};
undefinedundefined// set user["likes birds"] = true;
undefinedundefined// get alert(user["likes birds"]); // true
undefinedundefined// delete delete user["likes birds"];
undefinedundefinedNow everything is fine. Please note that the string inside the brackets is properly quoted (any type of quotes will do).
undefinedundefinedSquare brackets also provide a way to obtain the property name as the result of any expression - as opposed to a literal string - like from a variable as follows:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet key undefinedundefined=undefinedundefined"likes birds"undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// same as user["likes birds"] = true;undefinedundefinedundefinedundefineduser[key] undefinedundefined=undefinedundefinedtrueundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Here, the variable
undefinedundefinedkey may be calculated at run-time
or depend on the user input. And then we use it to access the
property. That gives us a great deal of
flexibility.undefinedundefined
For instance:
undefinedundefinedrun let user = { name: "John", age: 30 };
undefinedundefinedlet key = prompt("What do you want to know about the user?", "name");
undefinedundefined// access by variable alert( user[key] ); // John (if enter "name")
undefinedundefinedThe dot notation cannot be used in a similar way:
undefinedundefinedrun let user = { name: "John", age: 30 };
undefinedundefinedlet key = "name"; alert( user.key ) // undefined
undefinedundefinedWe can use square brackets in an object literal, when creating an object. That's called undefinedundefinedcomputed properties.undefinedundefined
undefinedundefinedFor instance:
undefinedundefinedrun let fruit = prompt("Which fruit to buy?", "apple");
undefinedundefinedlet bag = { undefinedundefined! [fruit]: 5, // the name of the property is taken from the variable fruit undefinedundefined/! };undefinedundefined
undefinedundefinedalert( bag.apple ); // 5 if fruit="apple"
undefinedundefinedThe meaning of a computed property is
simple: undefinedundefined[fruit] means that the
property name should be taken from
undefinedundefinedfruit.undefinedundefined
So, if a visitor enters
undefinedundefined"apple",
undefinedundefinedbag will become
undefinedundefined{apple: 5}.undefinedundefined
Essentially, that works the same as: run let fruit = prompt("Which fruit to buy?", "apple"); let bag = {};
undefinedundefined// take property name from the fruit variable bag[fruit] = 5;
undefinedundefined…But looks nicer.
undefinedundefinedWe can use more complex expressions inside square brackets:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet fruit undefinedundefined=undefinedundefined'apple'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedlet bag undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefined [fruit undefinedundefined+undefinedundefined'Computers']undefinedundefined:undefinedundefined5undefinedundefined// bag.appleComputers = 5undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Square brackets are much more powerful than the dot notation. They allow any property names and variables. But they are also more cumbersome to write.
undefinedundefinedSo most of the time, when property names are known and simple, the dot is used. And if we need something more complex, then we switch to square brackets.
undefinedundefinedIn real code we often use existing variables as values for property names.
undefinedundefinedFor instance:
undefinedundefinedrun function makeUser(name, age) { return { name: name, age: age, // …other properties }; }
undefinedundefinedlet user = makeUser("John", 30); alert(user.name); // John
undefinedundefinedIn the example above, properties have the same names as variables. The use-case of making a property from a variable is so common, that there's a special undefinedundefinedproperty value shorthand to make it shorter.undefinedundefined
undefinedundefinedInstead of
undefinedundefinedname:name we can just write
undefinedundefinedname, like
this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedmakeUser(nameundefinedundefined, age) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefined{undefinedundefinedundefinedundefined nameundefinedundefined,undefinedundefined// same as name: nameundefinedundefinedundefinedundefined ageundefinedundefined,undefinedundefined// same as age: ageundefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
We can use both normal properties and shorthands in the same object:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefined nameundefinedundefined,undefinedundefined// same as name:nameundefinedundefinedundefinedundefinedundefinedundefinedageundefinedundefined:undefinedundefined30undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
As we already know, a variable cannot have a name equal to one of language-reserved words like "for", "let", "return" etc.
undefinedundefinedBut for an object property, there's no such restriction:
undefinedundefinedrun // these properties are all right let obj = { for: 1, let: 2, return: 3 };
undefinedundefinedalert( obj.for + obj.let + obj.return ); // 6
undefinedundefinedIn short, there are no limitations on property names. They can be any strings or symbols (a special type for identifiers, to be covered later).
undefinedundefinedOther types are automatically converted to strings.
undefinedundefinedFor instance, a number
undefinedundefined0 becomes a string
undefinedundefined"0" when used as a property
key:undefinedundefined
run let obj = { 0: "test" // same as "0": "test" };
undefinedundefined// both alerts access the same property (the number 0 is converted to string "0") alert( obj["0"] ); // test alert( obj[0] ); // test (same property)
undefinedundefinedThere's a minor gotcha with a special
property named undefinedundefined__proto__. We
can't set it to a non-object value:undefinedundefined
undefinedundefinedjs run let obj = {}; obj.__proto__ = 5; // assign a number alert(obj.__proto__); // [object Object] - the value is an object, didn't work as intendedundefinedundefined
As we see from the code, the assignment
to a primitive undefinedundefined5 is
ignored.undefinedundefined
We'll cover
the special nature of undefinedundefined__proto__
in undefinedundefinedsubsequent chapters, and
suggest the undefinedundefinedways to fix such
behavior.undefinedundefined
A notable feature of objects in JavaScript, compared to many other languages, is that it's possible to access any property. There will be no error if the property doesn't exist!
undefinedundefined
Reading a non-existing property just returns
undefinedundefinedundefined. So we can easily test
whether the property exists:undefinedundefined
run let user = {};
undefinedundefinedalert( user.noSuchProperty === undefined ); // true means "no such property"
undefinedundefinedThere's also a special operator
undefinedundefined"in" for that.undefinedundefined
The syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined"key"undefinedundefinedin objectundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
For instance:
undefinedundefinedrun let user = { name: "John", age: 30 };
undefinedundefinedalert( "age" in user ); // true, user.age exists alert( "blabla" in user ); // false, user.blabla doesn't exist
undefinedundefinedPlease note that on the left side of
undefinedundefinedin there must be a
undefinedundefinedproperty name. That's usually a
quoted string.undefinedundefined
If we omit quotes, that means a variable, it should contain the actual name to be tested. For instance:
undefinedundefinedrun let user = { age: 30 };
undefinedundefinedlet key = "age"; alert( undefinedundefined!keyundefinedundefined/! in user ); // true, property "age" exists undefinedundefined
undefinedundefinedWhy does the
undefinedundefinedin operator exist? Isn't it
enough to compare against
undefinedundefinedundefined?undefinedundefined
Well, most of the time the comparison with
undefinedundefinedundefined works fine. But there's
a special case when it fails, but
undefinedundefined"in" works
correctly.undefinedundefined
It's when
an object property exists, but stores
undefinedundefinedundefined:undefinedundefined
run let obj = { test: undefined };
undefinedundefinedalert( obj.test ); // it's undefined, so - no such property?
undefinedundefinedalert( "test" in obj ); // true, the property does exist!
undefinedundefinedIn the code above, the property
undefinedundefinedobj.test technically exists. So
the undefinedundefinedin operator works
right.undefinedundefined
Situations like
this happen very rarely, because
undefinedundefinedundefined should not be
explicitly assigned. We mostly use
undefinedundefinednull for "unknown" or "empty"
values. So the undefinedundefinedin operator is an
exotic guest in the code.undefinedundefined
To walk over all keys of an object,
there exists a special form of the loop:
undefinedundefinedfor..in. This is a completely
different thing from the undefinedundefinedfor(;;)
construct that we studied before.undefinedundefined
The syntax:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfor (key undefinedundefinedin object) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// executes the body for each key among object propertiesundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
For instance, let's output all
properties of
undefinedundefineduser:undefinedundefined
run let user = { name: "John", age: 30, isAdmin: true };
undefinedundefinedfor (let key in user) { // keys alert( key ); // name, age, isAdmin // values for the keys alert( user[key] ); // John, 30, true }
undefinedundefinedNote that all "for" constructs allow us
to declare the looping variable inside the loop, like
undefinedundefinedlet key here.undefinedundefined
Also, we could use another variable name
here instead of undefinedundefinedkey. For
instance, undefinedundefined"for (let prop in obj)"
is also widely used.undefinedundefined
Are objects ordered? In other words, if we loop over an object, do we get all properties in the same order they were added? Can we rely on this?
undefinedundefinedThe short answer is: "ordered in a special fashion": integer properties are sorted, others appear in creation order. The details follow.
undefinedundefinedAs an example, let's consider an object with the phone codes:
undefinedundefinedrun let codes = { "49": "Germany", "41": "Switzerland", "44": "Great Britain", // .., "1": "USA" };
undefinedundefinedundefinedundefined! for (let code in codes) { alert(code); // 1, 41, 44, 49 } undefinedundefined/! undefinedundefined
undefinedundefinedThe object may be
used to suggest a list of options to the user. If we're making a
site mainly for German audience then we probably want
undefinedundefined49 to be the
first.undefinedundefined
But if we run the code, we see a totally different picture:
undefinedundefinedThe phone codes go
in the ascending sorted order, because they are integers. So we
see
undefinedundefined1, 41, 44, 49.undefinedundefined
smart header="Integer properties? What's that?" The "integer property" term here means a string that can be converted to-and-from an integer without a change.
undefinedundefinedSo, "49" is an integer property name, because when it's transformed to an integer number and back, it's still the same. But "+49" and "1.2" are not:
undefinedundefined
undefinedundefinedjs run // Math.trunc is a built-in function that removes the decimal part alert( String(Math.trunc(Number("49"))) ); // "49", same, integer property alert( String(Math.trunc(Number("+49"))) ); // "49", not same "+49" ⇒ not integer property alert( String(Math.trunc(Number("1.2"))) ); // "1", not same "1.2" ⇒ not integer property
undefinedundefined
…On the other hand, if the keys are non-integer, then they are listed in the creation order, for instance:
undefinedundefinedrun let user = { name: "John", surname: "Smith" }; user.age = 25; // add one more
undefinedundefinedundefinedundefined! // non-integer properties are listed in the creation order undefinedundefined/! for (let prop in user) { alert( prop ); // name, surname, age } undefinedundefined
undefinedundefinedSo, to fix the issue
with the phone codes, we can "cheat" by making the codes
non-integer. Adding a plus undefinedundefined"+"
sign before each code is enough.undefinedundefined
Like this:
undefinedundefinedrun let codes = { "+49": "Germany", "+41": "Switzerland", "+44": "Great Britain", // .., "+1": "USA" };
undefinedundefinedfor (let code in codes) { alert( +code ); // 49, 41, 44, 1 }
undefinedundefinedNow it works as intended.
undefinedundefinedObjects are associative arrays with several special features.
undefinedundefinedThey store properties (key-value pairs), where: - Property keys must be strings or symbols (usually strings). - Values can be of any type.
undefinedundefined
To access a property, we can use: - The dot notation:
undefinedundefinedobj.property. - Square brackets
notation undefinedundefinedobj["property"]. Square
brackets allow to take the key from a variable, like
undefinedundefinedobj[varWithKey].undefinedundefined
Additional operators: - To delete a
property: undefinedundefineddelete obj.prop. - To
check if a property with the given key exists:
undefinedundefined"key" in obj. - To iterate over
an object: undefinedundefinedfor (let key in obj)
loop.undefinedundefined
What we've
studied in this chapter is called a "plain object", or just
undefinedundefinedObject.undefinedundefined
There are many other kinds of objects in JavaScript:
undefinedundefinedArray to store ordered data
collections,undefinedundefinedDate to store the information
about the date and time,undefinedundefinedError to
store the information about an error.undefinedundefinedThey have their special features that we'll study later. Sometimes people say something like "Array type" or "Date type", but formally they are not types of their own, but belong to a single "object" data type. And they extend it in various ways.
undefinedundefinedObjects in JavaScript are very powerful. Here we've just scratched the surface of a topic that is really huge. We'll be closely working with objects and learning more about them in further parts of the tutorial.
undefinedundefinedA code editor is the place where programmers spend most of their time.
undefinedundefinedThere are two main types of code editors: IDEs and lightweight editors. Many people use one tool of each type.
undefinedundefinedThe term undefinedundefinedIDE (Integrated Development Environment) refers to a powerful editor with many features that usually operates on a "whole project." As the name suggests, it's not just an editor, but a full-scale "development environment."undefinedundefined
undefinedundefinedAn IDE loads the project (which can be many files), allows navigation between files, provides autocompletion based on the whole project (not just the open file), and integrates with a version management system (like undefinedundefinedgit), a testing environment, and other "project-level" stuff.undefinedundefined
undefinedundefinedIf you haven't selected an IDE yet, consider the following options:
undefinedundefinedFor Windows, there's also "Visual Studio", not to be confused with "Visual Studio Code". "Visual Studio" is a paid and mighty Windows-only editor, well-suited for the .NET platform. It's also good at JavaScript. There's also a free version undefinedundefinedVisual Studio Community.undefinedundefined
undefinedundefinedMany IDEs are paid, but have a trial period. Their cost is usually negligible compared to a qualified developer's salary, so just choose the best one for you.
undefinedundefined"Lightweight editors" are not as powerful as IDEs, but they're fast, elegant and simple.
undefinedundefinedThey are mainly used to open and edit a file instantly.
undefinedundefinedThe main difference between a "lightweight editor" and an "IDE" is that an IDE works on a project-level, so it loads much more data on start, analyzes the project structure if needed and so on. A lightweight editor is much faster if we need only one file.
undefinedundefinedIn practice, lightweight editors may have a lot of plugins including directory-level syntax analyzers and autocompleters, so there's no strict border between a lightweight editor and an IDE.
undefinedundefinedThe following options deserve your attention:
undefinedundefinedThe editors in the lists above are those that either I or my friends whom I consider good developers have been using for a long time and are happy with.
undefinedundefinedThere are other great editors in our big world. Please choose the one you like the most.
undefinedundefinedThe choice of an editor, like any other tool, is individual and depends on your projects, habits, and personal preferences.
undefinedundefinedOne of the fundamental differences of objects versus primitives is that objects are stored and copied "by reference", whereas primitive values: strings, numbers, booleans, etc - are always copied "as a whole value".
undefinedundefinedThat's easy to understand if we look a bit under the hood of what happens when we copy a value.
undefinedundefinedLet's start with a primitive, such as a string.
undefinedundefinedHere we put a copy of
undefinedundefinedmessage into
undefinedundefinedphrase:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet message undefinedundefined=undefinedundefined"Hello!"undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedlet phrase undefinedundefined= messageundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
As a result we have two independent
variables, each one storing the string
undefinedundefined"Hello!".undefinedundefined
undefinedundefinedundefinedundefined
Quite an obvious result, right?
undefinedundefinedObjects are not like that.
undefinedundefinedundefinedundefinedA variable assigned to an object stores not the object itself, but its "address in memory" - in other words "a reference" to it.undefinedundefined
undefinedundefinedLet's look at an example of such a variable:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
And here's how it's actually stored in memory:
undefinedundefinedundefinedundefinedundefinedundefined
The object is stored somewhere in memory (at
the right of the picture), while the
undefinedundefineduser variable (at the left) has a
"reference" to it.undefinedundefined
We
may think of an object variable, such as
undefinedundefineduser, as like a sheet of paper
with the address of the object on it.undefinedundefined
When we perform actions with the object,
e.g. take a property undefinedundefineduser.name,
the JavaScript engine looks at what's at that address and
performs the operation on the actual object.undefinedundefined
Now here's why it's important.
undefinedundefinedundefinedundefinedWhen an object variable is copied, the reference is copied, but the object itself is not duplicated.undefinedundefined
undefinedundefinedFor instance:
undefinedundefinedno-beautify let user = { name: "John" };
undefinedundefinedlet admin = user; // copy the reference
undefinedundefinedNow we have two variables, each storing a reference to the same object:
undefinedundefined
undefinedundefinedundefinedundefined
As you can see, there's still one object, but now with two variables that reference it.
undefinedundefinedWe can use either variable to access the object and modify its contents:
undefinedundefinedrun let user = { name: ‘John' };
undefinedundefinedlet admin = user;
undefinedundefinedundefinedundefined! admin.name = ‘Pete'; // changed by the "admin" reference undefinedundefined/!undefinedundefined
undefinedundefinedalert(undefinedundefined!user.nameundefinedundefined/!); // ‘Pete', changes are seen from the "user" reference undefinedundefined
undefinedundefinedIt's as if we had a
cabinet with two keys and used one of them
(undefinedundefinedadmin) to get into it and make
changes. Then, if we later use another key
(undefinedundefineduser), we are still opening the
same cabinet and can access the changed
contents.undefinedundefined
Two objects are equal only if they are the same object.
undefinedundefinedFor instance, here
undefinedundefineda and
undefinedundefinedb reference the same object, thus
they are equal:undefinedundefined
run let a = {}; let b = a; // copy the reference
undefinedundefinedalert( a == b ); // true, both variables reference the same object alert( a === b ); // true
undefinedundefinedAnd here two independent objects are not equal, even though they look alike (both are empty):
undefinedundefinedrun let a = {}; let b = {}; // two independent objects
undefinedundefinedalert( a == b ); // false
undefinedundefinedFor comparisons like
undefinedundefinedobj1 > obj2 or for a
comparison against a primitive
undefinedundefinedobj == 5, objects are converted
to primitives. We'll study how object conversions work very
soon, but to tell the truth, such comparisons are needed very
rarely - usually they appear as a result of a programming
mistake.undefinedundefined
So, copying an object variable creates one more reference to the same object.
undefinedundefinedBut what if we need to duplicate an object? Create an independent copy, a clone?
undefinedundefinedThat's also doable, but a little bit more difficult, because there's no built-in method for that in JavaScript. But there is rarely a need - copying by reference is good most of the time.
undefinedundefinedBut if we really want that, then we need to create a new object and replicate the structure of the existing one by iterating over its properties and copying them on the primitive level.
undefinedundefinedLike this:
undefinedundefinedrun let user = { name: "John", age: 30 };
undefinedundefinedundefinedundefined! let clone = {}; // the new empty objectundefinedundefined
undefinedundefined// let's copy all user properties into it for (let key in user) { clone[key] = user[key]; } undefinedundefined/!undefinedundefined
undefinedundefined// now clone is a fully independent object with the same content clone.name = "Pete"; // changed the data in it
undefinedundefinedalert( user.name ); // still John in the original object
undefinedundefinedAlso we can use the method undefinedundefinedObject.assign for that.undefinedundefined
undefinedundefinedThe syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedObject.undefinedundefinedassign(destundefinedundefined, [src1undefinedundefined, src2undefinedundefined,undefinedundefinedsrc3...])undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
dest is a target
object.undefinedundefinedsrc1, ..., srcN (can
be as many as needed) are source objects.undefinedundefined
src1, ..., srcN
into the target undefinedundefineddest. In other
words, properties of all arguments starting from the second
are copied into the first object.undefinedundefineddest.undefinedundefinedFor instance, we can use it to merge several objects into one:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{undefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet permissions1 undefinedundefined=undefinedundefined{undefinedundefinedcanViewundefinedundefined:undefinedundefinedtrueundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedlet permissions2 undefinedundefined=undefinedundefined{undefinedundefinedcanEditundefinedundefined:undefinedundefinedtrueundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefined// copies all properties from permissions1 and permissions2 into userundefinedundefinedundefinedundefinedundefinedundefinedObject.undefinedundefinedassign(userundefinedundefined, permissions1undefinedundefined, permissions2)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// now user = { name: "John", canView: true, canEdit: true }undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
If the copied property name already exists, it gets overwritten:
undefinedundefinedrun let user = { name: "John" };
undefinedundefinedObject.assign(user, { name: "Pete" });
undefinedundefinedalert(user.name); // now user = { name: "Pete" }
undefinedundefinedWe also can use
undefinedundefinedObject.assign to replace
undefinedundefinedfor..in loop for simple
cloning:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedageundefinedundefined:undefinedundefined30undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedlet clone undefinedundefined=undefinedundefinedObject.undefinedundefinedassign(undefinedundefined{}, user)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
It copies all properties of
undefinedundefineduser into the empty object and
returns it.undefinedundefined
There are
also other methods of cloning an object, e.g. using the
undefinedundefinedspread
syntaxundefinedundefinedclone = {...user},
covered later in the tutorial.undefinedundefined
Until now we assumed that all properties of
undefinedundefineduser are primitive. But
properties can be references to other objects. What to do with
them?undefinedundefined
Like this: run let user = { name: "John", sizes: { height: 182, width: 50 } };
undefinedundefinedalert( user.sizes.height ); // 182
undefinedundefinedNow it's not enough to copy
undefinedundefinedclone.sizes = user.sizes, because
the undefinedundefineduser.sizes is an object, it
will be copied by reference. So
undefinedundefinedclone and
undefinedundefineduser will share the same
sizes:undefinedundefined
Like this:
undefinedundefinedrun let user = { name: "John", sizes: { height: 182, width: 50 } };
undefinedundefinedlet clone = Object.assign({}, user);
undefinedundefinedalert( user.sizes === clone.sizes ); // true, same object
undefinedundefined// user and clone share sizes user.sizes.width++; // change a property from one place alert(clone.sizes.width); // 51, see the result from the other one
undefinedundefinedTo fix that, we should use a cloning loop
that examines each value of
undefinedundefineduser[key] and, if it's an object,
then replicate its structure as well. That is called a "deep
cloning".undefinedundefined
We can use recursion to implement it. Or, to not reinvent the wheel, take an existing implementation, for instance undefinedundefined_.cloneDeep(obj) from the JavaScript library undefinedundefinedlodash.undefinedundefined
undefinedundefined
undefinedundefinedsmart header="Const objects can be modified" An important side effect of storing objects as references is that an object declared asconst`
undefinedundefinedcan be modified.undefinedundefined
For instance:
undefinedundefinedrun const user = { name: "John" };
undefinedundefinedundefinedundefined! user.name = "Pete"; // (undefinedundefined) /!*undefinedundefined
undefinedundefinedalert(user.name); // Pete
undefinedundefinedundefinedundefined
It might seem that the line `(*)` would cause an error, but it does not. The value of `user` is constant, it must always reference the same object, but properties of that object are free to change.
In other words, the `const user` gives an error only if we try to set `user=...` as a whole.
That said, if we really need to make constant object properties, it's also possible, but using totally different methods. We'll mention that in the chapter <info:property-descriptors>.undefinedundefined
undefinedundefinedObjects are assigned and copied by reference. In other words, a variable stores not the "object value", but a "reference" (address in memory) for the value. So copying such a variable or passing it as a function argument copies that reference, not the object itself.
undefinedundefinedAll operations via copied references (like adding/removing properties) are performed on the same single object.
undefinedundefinedTo make a "real
copy" (a clone) we can use
undefinedundefinedObject.assign for the so-called
"shallow copy" (nested objects are copied by reference) or a
"deep cloning" function, such as undefinedundefined_.cloneDeep(obj).undefinedundefined
Memory management in JavaScript is performed automatically and invisibly to us. We create primitives, objects, functions… All that takes memory.
undefinedundefinedWhat happens when something is not needed any more? How does the JavaScript engine discover it and clean it up?
undefinedundefinedThe main concept of memory management in JavaScript is undefinedundefinedreachability.undefinedundefined
undefinedundefinedSimply put, "reachable" values are those that are accessible or usable somehow. They are guaranteed to be stored in memory.
undefinedundefinedThere's a base set of inherently reachable values, that cannot be deleted for obvious reasons.
undefinedundefinedFor instance:
undefinedundefinedThese values are called undefinedundefinedroots.undefinedundefined
undefinedundefinedAny other value is considered reachable if it's reachable from a root by a reference or by a chain of references.
undefinedundefinedFor instance, if there's an object in a global variable, and that object has a property referencing another object, undefinedundefinedthat object is considered reachable. And those that it references are also reachable. Detailed examples to follow.undefinedundefined
undefinedundefinedThere's a background process in the JavaScript engine that is called undefinedundefinedgarbage collector. It monitors all objects and removes those that have become unreachable.undefinedundefined
undefinedundefinedHere's the simplest example:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// user has a reference to the objectundefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedundefinedundefined
Here the arrow depicts an object reference.
The global variable undefinedundefined"user"
references the object
undefinedundefined{name: "John"} (we'll call it
John for brevity). The undefinedundefined"name"
property of John stores a primitive, so it's painted inside the
object.undefinedundefined
If the value
of undefinedundefineduser is overwritten, the
reference is lost:undefinedundefined
undefinedundefinedundefinedundefineduser undefinedundefined=undefinedundefinednullundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedundefinedundefined
Now John becomes unreachable. There's no way to access it, no references to it. Garbage collector will junk the data and free the memory.
undefinedundefinedNow
let's imagine we copied the reference from
undefinedundefineduser to
undefinedundefinedadmin:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// user has a reference to the objectundefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedlet admin undefinedundefined= userundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedundefinedundefined
Now if we do the same:
undefinedundefinedundefinedundefinedundefinedundefineduser undefinedundefined=undefinedundefinednullundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…Then the object is still reachable via
undefinedundefinedadmin global variable, so it's in
memory. If we overwrite undefinedundefinedadmin
too, then it can be removed.undefinedundefined
Now a more complex example. The family:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedmarry(manundefinedundefined, woman) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedwoman.undefinedundefinedhusbandundefinedundefined= manundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedman.undefinedundefinedwifeundefinedundefined= womanundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedfatherundefinedundefined: manundefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedmotherundefinedundefined: womanundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet family undefinedundefined=undefinedundefinedmarry(undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefinedundefinedundefinedundefinedundefined},undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined"Ann"undefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Function
undefinedundefinedmarry "marries" two objects by
giving them references to each other and returns a new object
that contains them both.undefinedundefined
The resulting memory structure:
undefinedundefined
undefinedundefinedundefinedundefined
As of now, all objects are reachable.
undefinedundefinedNow let's remove two references:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefineddeleteundefinedundefinedfamily.undefinedundefinedfatherundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefineddeleteundefinedundefinedfamily.undefinedundefinedmother.undefinedundefinedhusbandundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedundefinedundefined
It's not enough to delete only one of these two references, because all objects would still be reachable.
undefinedundefinedBut if we delete both, then we can see that John has no incoming reference any more:
undefinedundefinedundefinedundefinedundefinedundefined
Outgoing references do not matter. Only incoming ones can make an object reachable. So, John is now unreachable and will be removed from the memory with all its data that also became unaccessible.
undefinedundefinedAfter garbage collection:
undefinedundefined
undefinedundefinedundefinedundefined
It is possible that the whole island of interlinked objects becomes unreachable and is removed from the memory.
undefinedundefinedThe source object is the same as above. Then:
undefinedundefinedundefinedundefinedundefinedundefinedfamily undefinedundefined=undefinedundefinednullundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The in-memory picture becomes:
undefinedundefinedundefinedundefinedundefinedundefined
This example demonstrates how important the concept of reachability is.
undefinedundefinedIt's obvious that John and Ann are still linked, both have incoming references. But that's not enough.
undefinedundefinedThe
former undefinedundefined"family" object has been
unlinked from the root, there's no reference to it any more, so
the whole island becomes unreachable and will be
removed.undefinedundefined
The basic garbage collection algorithm is called "mark-and-sweep".
undefinedundefinedThe following "garbage collection" steps are regularly performed:
undefinedundefinedFor instance, let our object structure look like this:
undefinedundefinedundefinedundefinedundefinedundefined
We can clearly see an "unreachable island" to the right side. Now let's see how "mark-and-sweep" garbage collector deals with it.
undefinedundefinedThe first step marks the roots:
undefinedundefinedundefinedundefinedundefinedundefined
Then their references are marked:
undefinedundefinedundefinedundefinedundefinedundefined
…And their references, while possible:
undefinedundefinedundefinedundefinedundefinedundefined
Now the objects that could not be visited in the process are considered unreachable and will be removed:
undefinedundefinedundefinedundefinedundefinedundefined
We can also imagine the process as spilling a huge bucket of paint from the roots, that flows through all references and marks all reachable objects. The unmarked ones are then removed.
undefinedundefinedThat's the concept of how garbage collection works. JavaScript engines apply many optimizations to make it run faster and not affect the execution.
undefinedundefinedSome of the optimizations:
undefinedundefinedThere exist other optimizations and flavours of garbage collection algorithms. As much as I'd like to describe them here, I have to hold off, because different engines implement different tweaks and techniques. And, what's even more important, things change as engines develop, so studying deeper "in advance", without a real need is probably not worth that. Unless, of course, it is a matter of pure interest, then there will be some links for you below.
undefinedundefinedThe main things to know:
undefinedundefinedModern engines implement advanced algorithms of garbage collection.
undefinedundefinedA general book "The Garbage Collection Handbook: The Art of Automatic Memory Management" (R. Jones et al) covers some of them.
undefinedundefinedIf you are familiar with low-level programming, the more detailed information about V8 garbage collector is in the article undefinedundefinedA tour of V8: Garbage Collection.undefinedundefined
undefinedundefinedundefinedundefinedV8 blog also publishes articles about changes in memory management from time to time. Naturally, to learn the garbage collection, you'd better prepare by learning about V8 internals in general and read the blog of undefinedundefinedVyacheslav Egorov who worked as one of V8 engineers. I'm saying: "V8", because it is best covered with articles in the internet. For other engines, many approaches are similar, but garbage collection differs in many aspects.undefinedundefined
undefinedundefinedIn-depth knowledge of engines is good when you need low-level optimizations. It would be wise to plan that as the next step after you're familiar with the language.
undefinedundefinedObjects are usually created to represent entities of the real world, like users, orders and so on:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedageundefinedundefined:undefinedundefined30undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
And, in the real world, a user can undefinedundefinedact: select something from the shopping cart, login, logout etc.undefinedundefined
undefinedundefinedActions are represented in JavaScript by functions in properties.
undefinedundefined
For a start, let's teach the undefinedundefineduser
to say hello:undefinedundefined
run let user = { name: "John", age: 30 };
undefinedundefinedundefinedundefined! user.sayHi = function() { alert("Hello!"); }; undefinedundefined/!undefinedundefined
undefinedundefineduser.sayHi(); // Hello!
undefinedundefinedHere we've just used a Function
Expression to create a function and assign it to the property
undefinedundefineduser.sayHi of the
object.undefinedundefined
Then we can
call it as undefinedundefineduser.sayHi(). The user
can now speak!undefinedundefined
A function that is a property of an object is called its undefinedundefinedmethod.undefinedundefined
undefinedundefinedSo, here we've got a method
undefinedundefinedsayHi of the object
undefinedundefineduser.undefinedundefined
Of course, we could use a pre-declared function as a method, like this:
undefinedundefinedrun let user = { // … };
undefinedundefinedundefinedundefined! // first, declare function sayHi() { alert("Hello!"); };undefinedundefined
undefinedundefined// then add as a method user.sayHi = sayHi; undefinedundefined/!undefinedundefined
undefinedundefineduser.sayHi(); // Hello!
undefinedundefinedsmart header="Object-oriented programming" When we write our code using objects to represent entities, that's called undefinedundefinedobject-oriented programming, in short: "OOP".undefinedundefined
undefinedundefinedOOP is a big thing, an interesting science of its own. How to choose the right entities? How to organize the interaction between them? That's architecture, and there are great books on that topic, like "Design Patterns: Elements of Reusable Object-Oriented Software" by E. Gamma, R. Helm, R. Johnson, J. Vissides or "Object-Oriented Analysis and Design with Applications" by G. Booch, and more. ### Method shorthand
undefinedundefinedThere exists a shorter syntax for methods in an object literal:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// these objects do the sameundefinedundefinedundefinedundefinedundefinedundefineduser undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedsayHiundefinedundefined:undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined"Hello")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// method shorthand looks better, right?undefinedundefinedundefinedundefineduser undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedsayHi() undefinedundefined{undefinedundefined// same as "sayHi: function(){...}"undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined alertundefinedundefined(undefinedundefined"Hello"undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
As demonstrated, we can omit
undefinedundefined"function" and just write
undefinedundefinedsayHi().undefinedundefined
To tell the truth, the notations are not fully identical. There are subtle differences related to object inheritance (to be covered later), but for now they do not matter. In almost all cases the shorter syntax is preferred.
undefinedundefinedIt's common that an object method needs to access the information stored in the object to do its job.
undefinedundefinedFor instance, the code inside
undefinedundefineduser.sayHi() may need the name of
the undefinedundefineduser.undefinedundefined
undefinedundefinedTo access the
object, a method can use the
undefinedundefinedthis
keyword.undefinedundefinedundefinedundefined
The value of
undefinedundefinedthis is the object "before dot",
the one used to call the method.undefinedundefined
For instance:
undefinedundefinedrun let user = { name: "John", age: 30,
undefinedundefinedsayHi() { undefinedundefined! // "this" is the "current object" alert(this.name); undefinedundefined/! }undefinedundefined
undefinedundefined};
undefinedundefineduser.sayHi(); // John
undefinedundefinedHere during the execution of
undefinedundefineduser.sayHi(), the value of
undefinedundefinedthis will be
undefinedundefineduser.undefinedundefined
Technically, it's also possible to access the
object without undefinedundefinedthis, by
referencing it via the outer variable:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedageundefinedundefined:undefinedundefined30undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedsayHi() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefineduser.undefinedundefinedname)undefinedundefined;undefinedundefined// "user" instead of "this"undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…But such code is unreliable. If we
decide to copy undefinedundefineduser to another
variable, e.g. undefinedundefinedadmin = user and
overwrite undefinedundefineduser with something
else, then it will access the wrong object.undefinedundefined
That's demonstrated below:
undefinedundefinedrun let user = { name: "John", age: 30,
undefinedundefinedsayHi() { undefinedundefined! alert( user.name ); // leads to an error undefinedundefined/! }undefinedundefined
undefinedundefined};
undefinedundefinedlet admin = user; user = null; // overwrite to make things obvious
undefinedundefinedundefinedundefined! admin.sayHi(); // TypeError: Cannot read property ‘name' of null undefinedundefined/! undefinedundefined
undefinedundefinedIf we used
undefinedundefinedthis.name instead of
undefinedundefineduser.name inside the
undefinedundefinedalert, then the code would
work.undefinedundefined
In JavaScript, keyword
undefinedundefinedthis behaves unlike most other
programming languages. It can be used in any function, even if
it's not a method of an object.undefinedundefined
There's no syntax error in the following example:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedsayHi() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefined*!*undefinedundefinedthisundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefined.name undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The value of
undefinedundefinedthis is evaluated during the
run-time, depending on the context.undefinedundefined
For instance, here the same function is assigned to two different objects and has different "this" in the calls:
undefinedundefinedrun let user = { name: "John" }; let admin = { name: "Admin" };
undefinedundefinedfunction sayHi() { alert( this.name ); }
undefinedundefinedundefinedundefined! // use the same function in two objects user.f = sayHi; admin.f = sayHi; undefinedundefined/!undefinedundefined
undefinedundefined// these calls have different this // "this" inside the function is the object "before the dot" user.f(); // John (this == user) admin.f(); // Admin (this == admin)
undefinedundefinedadminundefinedundefined‘f'; // Admin (dot or square brackets access the method - doesn't matter) undefinedundefined
undefinedundefinedThe rule is simple:
if undefinedundefinedobj.f() is called, then
undefinedundefinedthis is
undefinedundefinedobj during the call of
undefinedundefinedf. So it's either
undefinedundefineduser or
undefinedundefinedadmin in the example
above.undefinedundefined
undefinedundefinedsmart header="Calling without an object:this
== undefined`" We can even call the function without an object
at all:undefinedundefined
run function sayHi() { alert(this); }
undefinedundefinedsayHi(); // undefined
undefinedundefinedundefinedundefined
In this case `this` is `undefined` in strict mode. If we try to access `this.name`, there will be an error.
In non-strict mode the value of `this` in such case will be the *global object* (`window` in a browser, we'll get to it later in the chapter [](info:global-object)). This is a historical behavior that `"use strict"` fixes.
Usually such call is a programming error. If there's `this` inside a function, it expects to be called in an object context.undefinedundefined
undefinedundefined
``undefinedundefinedsmart header="The consequences of unboundthisundefinedundefined" If you come from another programming language, then you are probably used to the idea of a "boundthisundefinedundefined", where methods defined in an object always havethis`
referencing that object.undefinedundefined
In JavaScript undefinedundefinedthis is "free",
its value is evaluated at call-time and does not depend on where
the method was declared, but rather on what object is "before
the dot".undefinedundefined
The concept
of run-time evaluated undefinedundefinedthis has
both pluses and minuses. On the one hand, a function can be
reused for different objects. On the other hand, the greater
flexibility creates more possibilities for
mistakes.undefinedundefined
Here our position is not to judge whether this language design decision is good or bad. We'll understand how to work with it, how to get benefits and avoid problems.
undefinedundefinedArrow
functions are special: they don't have their "own"
undefinedundefinedthis. If we reference
undefinedundefinedthis from such a function, it's
taken from the outer "normal" function.undefinedundefined
For instance, here
undefinedundefinedarrow() uses
undefinedundefinedthis from the outer
undefinedundefineduser.sayHi()
method:undefinedundefined
run let user = { firstName: "Ilya", sayHi() { let arrow = () => alert(this.firstName); arrow(); } };
undefinedundefineduser.sayHi(); // Ilya
undefinedundefinedThat's a special feature of arrow
functions, it's useful when we actually do not want to have a
separate undefinedundefinedthis, but rather to take
it from the outer context. Later in the chapter
undefinedundefinedinfo:arrow-functions we'll go more deeply into
arrow functions.undefinedundefined
object.doSomething().undefinedundefined
this.undefinedundefinedThe value of
undefinedundefinedthis is defined at run-time. -
When a function is declared, it may use
undefinedundefinedthis, but that
undefinedundefinedthis has no value until the
function is called. - A function can be copied between objects.
- When a function is called in the "method" syntax:
undefinedundefinedobject.method(), the value of
undefinedundefinedthis during the call is
undefinedundefinedobject.undefinedundefined
Please note that arrow functions are special:
they have no undefinedundefinedthis. When
undefinedundefinedthis is accessed inside an arrow
function, it is taken from outside.undefinedundefined
The regular
undefinedundefined{...} syntax allows to create one
object. But often we need to create many similar objects, like
multiple users or menu items and so on.undefinedundefined
That can be done using constructor functions
and the undefinedundefined"new"
operator.undefinedundefined
Constructor functions technically are regular functions. There are two conventions though:
undefinedundefined"new"
operator.undefinedundefinedFor instance:
undefinedundefinedrun function User(name) { this.name = name; this.isAdmin = false; }
undefinedundefinedundefinedundefined! let user = new User("Jack"); undefinedundefined/!undefinedundefined
undefinedundefinedalert(user.name); // Jack alert(user.isAdmin); // false
undefinedundefinedWhen a function is executed with
undefinedundefinednew, it does the following
steps:undefinedundefined
this.undefinedundefinedthis, adds new
properties to it.undefinedundefinedthis is
returned.undefinedundefinedIn other words,
undefinedundefinednew User(...) does something
like:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedUser(name) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefined// this = {}; (implicitly)undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined // add properties to thisundefinedundefinedundefinedundefinedundefinedundefined this.name = name;undefinedundefinedundefinedundefinedundefinedundefined this.isAdmin = false;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined // return this; undefinedundefined(undefinedundefinedimplicitlyundefinedundefined)undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/undefinedundefined!*undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
So
undefinedundefinedlet user = new User("Jack") gives
the same result as:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined"Jack"undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedisAdminundefinedundefined:undefinedundefinedfalseundefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Now if we want to create other users,
we can call undefinedundefinednew User("Ann"),
undefinedundefinednew User("Alice") and so on. Much
shorter than using literals every time, and also easy to
read.undefinedundefined
That's the main purpose of constructors - to implement reusable object creation code.
undefinedundefinedLet's note once again -
technically, any function can be used as a constructor. That is:
any function can be run with undefinedundefinednew,
and it will execute the algorithm above. The "capital letter
first" is a common agreement, to make it clear that a function
is to be run with
undefinedundefinednew.undefinedundefined
smart header="new function() { … }" If we have many lines of code all about creation of a single complex object, we can wrap them in constructor function, like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefinednewundefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedthis.undefinedundefinednameundefinedundefined=undefinedundefined"John"undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedthis.undefinedundefinedisAdminundefinedundefined=undefinedundefinedfalseundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// ...other code for user creationundefinedundefinedundefinedundefinedundefinedundefined// maybe complex logic and statementsundefinedundefinedundefinedundefinedundefinedundefined// local variables etcundefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The constructor can't be called again, because it is not saved anywhere, just created and called. So this trick aims to encapsulate the code that constructs the single object, without future reuse.
undefinedundefined
undefinedundefinedsmart header="Advanced stuff" The syntax from this section is rarely used, skip it unless you want to know everything.undefinedundefined
Inside a function, we can check whether
it was called with undefinedundefinednew or without
it, using a special undefinedundefinednew.target
property.undefinedundefined
It is
undefined for regular calls and equals the function if called
with undefinedundefinednew:undefinedundefined
run function User() { alert(new.target); }
undefinedundefined// without "new": undefinedundefined! User(); // undefined undefinedundefined/!undefinedundefined
undefinedundefined// with "new": undefinedundefined! new User(); // function User { … } undefinedundefined/! undefinedundefined
undefinedundefinedThat can be used
inside the function to know whether it was called with
undefinedundefinednew, "in constructor mode", or
without it, "in regular mode".undefinedundefined
We can also make both
undefinedundefinednew and regular calls to do the
same, like this:undefinedundefined
run function User(name) { if (!new.target) { // if you run me without new return new User(name); // …I will add new for you }
undefinedundefinedthis.name = name; }
undefinedundefinedlet john = User("John"); // redirects call to new User alert(john.name); // John
undefinedundefinedThis approach is sometimes used in
libraries to make the syntax more flexible. So that people may
call the function with or without
undefinedundefinednew, and it still
works.undefinedundefined
Probably not a
good thing to use everywhere though, because omitting
undefinedundefinednew makes it a bit less obvious
what's going on. With undefinedundefinednew we all
know that the new object is being created.undefinedundefined
Usually, constructors do
not have a undefinedundefinedreturn statement.
Their task is to write all necessary stuff into
undefinedundefinedthis, and it automatically
becomes the result.undefinedundefined
But if there is a undefinedundefinedreturn
statement, then the rule is simple:undefinedundefined
return is called with an
object, then the object is returned instead of
undefinedundefinedthis.undefinedundefinedreturn
is called with a primitive, it's ignored.undefinedundefined
In other words,
undefinedundefinedreturn with an object returns
that object, in all other cases
undefinedundefinedthis is
returned.undefinedundefined
For
instance, here undefinedundefinedreturn overrides
undefinedundefinedthis by returning an
object:undefinedundefined
run function BigUser() {
undefinedundefinedthis.name = "John";
undefinedundefinedreturn { name: "Godzilla" }; // <- returns this object }
undefinedundefinedalert( new BigUser().name ); // Godzilla, got that object
undefinedundefinedAnd here's an example with an empty
undefinedundefinedreturn (or we could place a
primitive after it, doesn't matter):undefinedundefined
run function SmallUser() {
undefinedundefinedthis.name = "John";
undefinedundefinedreturn; // <- returns this }
undefinedundefinedalert( new SmallUser().name ); // John
undefinedundefinedUsually constructors don't have a
undefinedundefinedreturn statement. Here we mention
the special behavior with returning objects mainly for the sake
of completeness.undefinedundefined
undefinedundefinedsmart header="Omitting parentheses" By the way, we can omit parentheses afternew`,
if it has no arguments:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefinednew Userundefinedundefined;undefinedundefined// <-- no parenthesesundefinedundefinedundefinedundefinedundefinedundefined// same asundefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefinednewundefinedundefinedUser()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Omitting parentheses here is not considered a "good style", but the syntax is permitted by specification.
undefinedundefinedUsing constructor functions to create objects gives a great deal of flexibility. The constructor function may have parameters that define how to construct the object, and what to put in it.
undefinedundefinedOf course, we can add to
undefinedundefinedthis not only properties, but
methods as well.undefinedundefined
For
instance, undefinedundefinednew User(name) below
creates an object with the given
undefinedundefinedname and the method
undefinedundefinedsayHi:undefinedundefined
run function User(name) { this.name = name;
undefinedundefinedthis.sayHi = function() { alert( "My name is:" + this.name ); }; }
undefinedundefinedundefinedundefined! let john = new User("John");undefinedundefined
undefinedundefinedjohn.sayHi(); // My name is: John undefinedundefined/!undefinedundefined
undefinedundefined/undefinedundefined john = { name: "John", sayHi: function() { … } } / undefinedundefined
undefinedundefinedTo create complex objects, there's a more advanced syntax, undefinedundefinedclasses, that we'll cover later.undefinedundefined
undefinedundefinednew. Such a call implies a
creation of empty undefinedundefinedthis at the
start and returning the populated one at the
end.undefinedundefinedWe can use constructor functions to make multiple similar objects.
undefinedundefinedJavaScript
provides constructor functions for many built-in language
objects: like undefinedundefinedDate for dates,
undefinedundefinedSet for sets and others that we
plan to study.undefinedundefined
smart header="Objects, we'll be back!" In this chapter we only cover the basics about objects and constructors. They are essential for learning more about data types and functions in the next chapters.
undefinedundefinedAfter we learn that, we return to objects and cover them in-depth in the chapters undefinedundefinedinfo:prototypes and undefinedundefinedinfo:classes. undefinedundefined
undefinedundefined[recent browser="new"]
undefinedundefinedThe optional chaining undefinedundefined?. is a
safe way to access nested object properties, even if an
intermediate property doesn't exist.undefinedundefined
If you've just started to read the tutorial and learn JavaScript, maybe the problem hasn't touched you yet, but it's quite common.
undefinedundefinedAs an example, let's say we have
undefinedundefineduser objects that hold the
information about our users.undefinedundefined
Most of our users have addresses in
undefinedundefineduser.address property, with the
street undefinedundefineduser.address.street, but
some did not provide them.undefinedundefined
In such case, when we attempt to get
undefinedundefineduser.address.street, and the user
happens to be without an address, we get an
error:undefinedundefined
run let user = {}; // a user without "address" property
undefinedundefinedalert(user.address.street); // Error!
undefinedundefinedThat's the expected result. JavaScript
works like this. As undefinedundefineduser.address
is undefinedundefinedundefined, an attempt to get
undefinedundefineduser.address.street fails with an
error.undefinedundefined
In many
practical cases we'd prefer to get
undefinedundefinedundefined instead of an error
here (meaning "no street").undefinedundefined
…And another example. In the web development,
we can get an object that corresponds to a web page element
using a special method call, such as
undefinedundefineddocument.querySelector('.elem'),
and it returns undefinedundefinednull when there's
no such element.undefinedundefined
undefinedundefinedjs run // document.querySelector('.elem') is null if there's no element let html = document.querySelector('.elem').innerHTML; // error if it's nullundefinedundefined
Once again, if the element doesn't exist,
we'll get an error accessing
undefinedundefined.innerHTML of
undefinedundefinednull. And in some cases, when the
absence of the element is normal, we'd like to avoid the error
and just accept undefinedundefinedhtml = null as
the result.undefinedundefined
How can we do this?
undefinedundefinedThe obvious solution would be
to check the value using undefinedundefinedif or
the conditional operator undefinedundefined?,
before accessing its property, like this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefineduser.undefinedundefinedaddressundefinedundefined?undefinedundefineduser.undefinedundefinedaddress.undefinedundefinedstreet : undefinedundefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
It works, there's no error… But it's
quite inelegant. As you can see, the
undefinedundefined"user.address" appears twice in
the code. For more deeply nested properties, that becomes a
problem as more repetitions are required.undefinedundefined
E.g. let's try getting
undefinedundefineduser.address.street.name.undefinedundefined
We need to check both
undefinedundefineduser.address and
undefinedundefineduser.address.street:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{};undefinedundefined// user has no addressundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefineduser.undefinedundefinedaddressundefinedundefined?undefinedundefineduser.undefinedundefinedaddress.undefinedundefinedstreetundefinedundefined?undefinedundefineduser.undefinedundefinedaddress.undefinedundefinedstreet.undefinedundefinedname : undefinedundefinednull : undefinedundefinednull)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That's just awful, one may even have problems understanding such code.
undefinedundefinedDon't
even care to, as there's a better way to write it, using the
undefinedundefined&&
operator:undefinedundefined
run let user = {}; // user has no address
undefinedundefinedalert( user.address && user.address.street && user.address.street.name ); // undefined (no error)
undefinedundefinedAND'ing the whole path to the property ensures that all components exist (if not, the evaluation stops), but also isn't ideal.
undefinedundefinedAs you can
see, property names are still duplicated in the code. E.g. in
the code above, undefinedundefineduser.address
appears three times.undefinedundefined
That's why the optional chaining
undefinedundefined?. was added to the language. To
solve this problem once and for all!undefinedundefined
The optional chaining
undefinedundefined?. stops the evaluation if the
value before undefinedundefined?. is
undefinedundefinedundefined or
undefinedundefinednull and returns
undefinedundefinedundefined.undefinedundefined
undefinedundefinedFurther in this
article, for brevity, we'll be saying that something "exists"
if it's not undefinedundefinednull and not
undefinedundefinedundefined.undefinedundefinedundefinedundefined
In other words,
undefinedundefinedvalue?.prop: - works as
undefinedundefinedvalue.prop, if
undefinedundefinedvalue exists, - otherwise (when
undefinedundefinedvalue is
undefinedundefinedundefined/null) it returns
undefinedundefinedundefined.undefinedundefined
Here's the safe way to access
undefinedundefineduser.address.street using
undefinedundefined?.:undefinedundefined
run let user = {}; // user has no address
undefinedundefinedalert( user?.address?.street ); // undefined (no error)
undefinedundefinedThe code is short and clean, there's no duplication at all.
undefinedundefinedReading the address
with undefinedundefineduser?.address works even if
undefinedundefineduser object doesn't
exist:undefinedundefined
run let user = null;
undefinedundefinedalert( user?.address ); // undefined alert( user?.address.street ); // undefined
undefinedundefinedPlease note: the
undefinedundefined?. syntax makes optional the
value before it, but not any further.undefinedundefined
E.g. in
undefinedundefineduser?.address.street.name the
undefinedundefined?. allows
undefinedundefineduser to safely be
undefinedundefinednull/undefined (and returns
undefinedundefinedundefined in that case), but
that's only for undefinedundefineduser. Further
properties are accessed in a regular way. If we want some of
them to be optional, then we'll need to replace more
undefinedundefined. with
undefinedundefined?..undefinedundefined
``undefinedundefinedwarn header="Don't overuse the optional chaining" We should use?.`
only where it's ok that something doesn't
exist.undefinedundefined
For example, if
according to our coding logic
undefinedundefineduser object must exist, but
undefinedundefinedaddress is optional, then we
should write
undefinedundefineduser.address?.street, but not
undefinedundefineduser?.address?.street.undefinedundefined
So, if
undefinedundefineduser happens to be undefined due
to a mistake, we'll see a programming error about it and fix it.
Otherwise, coding errors can be silenced where not appropriate,
and become more difficult to debug.
undefinedundefined
undefinedundefinedwarn header="The variable before?.undefinedundefinedmust be declared" If there's no variableuserundefinedundefinedat all, thenuser?.anything`
triggers an error:undefinedundefined
undefinedundefinedjs run // ReferenceError: user is not defined user?.address;
The variable must be declared
(e.g. undefinedundefinedlet/const/var user or as a
function parameter). The optional chaining works only for
declared variables.
undefinedundefined
As it was said before, the undefinedundefined?.
immediately stops ("short-circuits") the evaluation if the left
part doesn't exist.undefinedundefined
So, if there are any further function calls or side effects, they don't occur.
undefinedundefinedFor instance:
undefinedundefinedrun let user = null; let x = 0;
undefinedundefineduser?.sayHi(x++); // no "sayHi", so the execution doesn't reach x++
undefinedundefinedalert(x); // 0, value not incremented
undefinedundefinedThe optional chaining
undefinedundefined?. is not an operator, but a
special syntax construct, that also works with functions and
square brackets.undefinedundefined
For
example, undefinedundefined?.() is used to call a
function that may not exist.undefinedundefined
In the code below, some of our users have
undefinedundefinedadmin method, and some
don't:undefinedundefined
run let userAdmin = { admin() { alert("I am admin"); } };
undefinedundefinedlet userGuest = {};
undefinedundefinedundefinedundefined! userAdmin.admin?.(); // I am admin undefinedundefined/!undefinedundefined
undefinedundefinedundefinedundefined! userGuest.admin?.(); // nothing (no such method) undefinedundefined/! undefinedundefined
undefinedundefinedHere, in both lines
we first use the dot
(undefinedundefineduserAdmin.admin) to get
undefinedundefinedadmin property, because we assume
that the user object exists, so it's safe read from
it.undefinedundefined
Then
undefinedundefined?.() checks the left part: if the
admin function exists, then it runs (that's so for
undefinedundefineduserAdmin). Otherwise (for
undefinedundefineduserGuest) the evaluation stops
without errors.undefinedundefined
The
undefinedundefined?.[] syntax also works, if we'd
like to use brackets undefinedundefined[] to access
properties instead of dot undefinedundefined..
Similar to previous cases, it allows to safely read a property
from an object that may not exist.undefinedundefined
run let key = "firstName";
undefinedundefinedlet user1 = { firstName: "John" };
undefinedundefinedlet user2 = null;
undefinedundefinedalert( user1?.[key] ); // John alert( user2?.[key] ); // undefined
undefinedundefinedAlso we can use
undefinedundefined?. with
undefinedundefineddelete:undefinedundefined
undefinedundefinedjs run delete user?.name; // delete user.name if user existsundefinedundefined
undefinedundefinedwarn header="We can use?.undefinedundefinedfor safe reading and deleting, but not writing" The optional chaining?.`
has no use at the left side of an assignment.undefinedundefined
For example: run let user = null;
undefinedundefineduser?.name = "John"; // Error, doesn't work // because it evaluates to undefined = "John"
undefinedundefinedundefinedundefined
It's just not that smart.undefinedundefinedundefinedundefinedThe optional chaining
undefinedundefined?. syntax has three
forms:undefinedundefined
obj?.prop -
returns undefinedundefinedobj.prop if
undefinedundefinedobj exists, otherwise
undefinedundefinedundefined.undefinedundefined
obj?.[prop] - returns
undefinedundefinedobj[prop] if
undefinedundefinedobj exists, otherwise
undefinedundefinedundefined.undefinedundefined
obj.method?.() - calls
undefinedundefinedobj.method() if
undefinedundefinedobj.method exists, otherwise
returns
undefinedundefinedundefined.undefinedundefined
As we can see,
all of them are straightforward and simple to use. The
undefinedundefined?. checks the left part for
undefinedundefinednull/undefined and allows the
evaluation to proceed if it's not so.undefinedundefined
A chain of undefinedundefined?.
allows to safely access nested properties.undefinedundefined
Still, we should apply
undefinedundefined?. carefully, only where it's
acceptable that the left part doesn't exist. So that it won't
hide programming errors from us, if they
occur.undefinedundefined
By specification, object property keys may be either of string type, or of symbol type. Not numbers, not booleans, only strings or symbols, these two types.
undefinedundefinedTill now we've been using only strings. Now let's see the benefits that symbols can give us.
undefinedundefinedA "symbol" represents a unique identifier.
undefinedundefinedA value of this type can be
created using
undefinedundefinedSymbol():undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// id is a new symbolundefinedundefinedundefinedundefinedundefinedundefinedlet id undefinedundefined=undefinedundefinedSymbol()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Upon creation, we can give symbol a description (also called a symbol name), mostly useful for debugging purposes:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// id is a symbol with the description "id"undefinedundefinedundefinedundefinedundefinedundefinedlet id undefinedundefined=undefinedundefinedSymbol(undefinedundefined"id")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Symbols are guaranteed to be unique. Even if we create many symbols with the same description, they are different values. The description is just a label that doesn't affect anything.
undefinedundefinedFor instance, here are two symbols with the same description - they are not equal:
undefinedundefinedrun let id1 = Symbol("id"); let id2 = Symbol("id");
undefinedundefinedundefinedundefined! alert(id1 == id2); // false undefinedundefined/! undefinedundefined
undefinedundefinedIf you are familiar with Ruby or another language that also has some sort of "symbols" - please don't be misguided. JavaScript symbols are different.
undefinedundefined
undefinedundefinedwarn header="Symbols don't auto-convert to a string" Most values in JavaScript support implicit conversion to a string. For instance, we canalert`
almost any value, and it will work. Symbols are special. They
don't auto-convert.undefinedundefined
For instance, this undefinedundefinedalert will
show an error:undefinedundefined
undefinedundefinedjs run let id = Symbol("id"); *!* alert(id); // TypeError: Cannot convert a Symbol value to a string */!*undefinedundefined
That's a "language guard" against messing up, because strings and symbols are fundamentally different and should not accidentally convert one into another.
undefinedundefinedIf we really want to show a symbol, we need
to explicitly call undefinedundefined.toString() on
it, like here:
undefinedundefinedjs run let id = Symbol("id"); *!* alert(id.toString()); // Symbol(id), now it works */!*undefinedundefined
Or get
undefinedundefinedsymbol.description property to
show the description only:
undefinedundefinedjs run let id = Symbol("id"); *!* alert(id.description); // id */!*undefinedundefined
undefinedundefined
Symbols allow us to create "hidden" properties of an object, that no other part of code can accidentally access or overwrite.
undefinedundefinedFor
instance, if we're working with
undefinedundefineduser objects, that belong to a
third-party code. We'd like to add identifiers to
them.undefinedundefined
Let's use a symbol key for it:
undefinedundefinedrun let user = { // belongs to another code name: "John" };
undefinedundefinedlet id = Symbol("id");
undefinedundefineduser[id] = 1;
undefinedundefinedalert( user[id] ); // we can access the data using the symbol as the key
undefinedundefinedWhat's the benefit of using
undefinedundefinedSymbol("id") over a string
undefinedundefined"id"?undefinedundefined
As undefinedundefineduser
objects belongs to another code, and that code also works with
them, we shouldn't just add any fields to it. That's unsafe. But
a symbol cannot be accessed accidentally, the third-party code
probably won't even see it, so it's probably all right to
do.undefinedundefined
Also, imagine that
another script wants to have its own identifier inside
undefinedundefineduser, for its own purposes. That
may be another JavaScript library, so that the scripts are
completely unaware of each other.undefinedundefined
Then that script can create its own
undefinedundefinedSymbol("id"), like
this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefinedlet id undefinedundefined=undefinedundefinedSymbol(undefinedundefined"id")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefineduser[id] undefinedundefined=undefinedundefined"Their id value"undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
There will be no conflict between our and their identifiers, because symbols are always different, even if they have the same name.
undefinedundefined…But if
we used a string undefinedundefined"id" instead of
a symbol for the same purpose, then there
undefinedundefinedwould be a
conflict:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{undefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// Our script uses "id" propertyundefinedundefinedundefinedundefinedundefinedundefineduser.undefinedundefinedidundefinedundefined=undefinedundefined"Our id value"undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// ...Another script also wants "id" for its purposes...undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefineduser.undefinedundefinedidundefinedundefined=undefinedundefined"Their id value"undefinedundefinedundefinedundefinedundefinedundefined// Boom! overwritten by another script!undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
If we want
to use a symbol in an object literal
undefinedundefined{...}, we need square brackets
around it.undefinedundefined
Like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet id undefinedundefined=undefinedundefinedSymbol(undefinedundefined"id")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefined [id]undefinedundefined:undefinedundefined123undefinedundefined// not "id": 123undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That's because we need the value from
the variable undefinedundefinedid as the key, not
the string "id".undefinedundefined
Symbolic properties do not
participate in undefinedundefinedfor..in
loop.undefinedundefined
For instance:
undefinedundefinedrun let id = Symbol("id"); let user = { name: "John", age: 30, [id]: 123 };
undefinedundefinedundefinedundefined! for (let key in user) alert(key); // name, age (no symbols) undefinedundefined/!undefinedundefined
undefinedundefined// the direct access by the symbol works alert( "Direct:" + user[id] );
undefinedundefined
undefinedundefinedObject.keys(user) also ignores
them. That's a part of the general "hiding symbolic properties"
principle. If another script or a library loops over our object,
it won't unexpectedly access a symbolic
property.undefinedundefined
In contrast, undefinedundefinedObject.assign copies both string and symbol properties:undefinedundefined
undefinedundefinedrun let id = Symbol("id"); let user = { [id]: 123 };
undefinedundefinedlet clone = Object.assign({}, user);
undefinedundefinedalert( clone[id] ); // 123
undefinedundefinedThere's no paradox here. That's by
design. The idea is that when we clone an object or merge
objects, we usually want undefinedundefinedall
properties to be copied (including symbols like
undefinedundefinedid).undefinedundefined
As we've seen, usually all symbols are
different, even if they have the same name. But sometimes we
want same-named symbols to be same entities. For instance,
different parts of our application want to access symbol
undefinedundefined"id" meaning exactly the same
property.undefinedundefined
To achieve that, there exists a undefinedundefinedglobal symbol registry. We can create symbols in it and access them later, and it guarantees that repeated accesses by the same name return exactly the same symbol.undefinedundefined
undefinedundefinedIn order to read (create if absent) a symbol
from the registry, use
undefinedundefinedSymbol.for(key).undefinedundefined
That call checks the global registry, and
if there's a symbol described as
undefinedundefinedkey, then returns it, otherwise
creates a new symbol undefinedundefinedSymbol(key)
and stores it in the registry by the given
undefinedundefinedkey.undefinedundefined
For instance:
undefinedundefinedrun // read from the global registry let id = Symbol.for("id"); // if the symbol did not exist, it is created
undefinedundefined// read it again (maybe from another part of the code) let idAgain = Symbol.for("id");
undefinedundefined// the same symbol alert( id === idAgain ); // true
undefinedundefinedSymbols inside the registry are called undefinedundefinedglobal symbols. If we want an application-wide symbol, accessible everywhere in the code - that's what they are for.undefinedundefined
undefinedundefinedsmart header="That sounds like Ruby" In some programming languages, like Ruby, there's a single symbol per name.
undefinedundefinedIn JavaScript, as we can see, that's right for global symbols.
undefinedundefinedFor global symbols, not only
undefinedundefinedSymbol.for(key) returns a symbol
by name, but there's a reverse call:
undefinedundefinedSymbol.keyFor(sym), that does the
reverse: returns a name by a global symbol.undefinedundefined
For instance:
undefinedundefinedrun // get symbol by name let sym = Symbol.for("name"); let sym2 = Symbol.for("id");
undefinedundefined// get name by symbol alert( Symbol.keyFor(sym) ); // name alert( Symbol.keyFor(sym2) ); // id
undefinedundefinedThe
undefinedundefinedSymbol.keyFor internally uses the
global symbol registry to look up the key for the symbol. So it
doesn't work for non-global symbols. If the symbol is not
global, it won't be able to find it and returns
undefinedundefinedundefined.undefinedundefined
That said, any symbols have
undefinedundefineddescription
property.undefinedundefined
For instance:
undefinedundefinedrun let globalSymbol = Symbol.for("name"); let localSymbol = Symbol("name");
undefinedundefinedalert( Symbol.keyFor(globalSymbol) ); // name, global symbol alert( Symbol.keyFor(localSymbol) ); // undefined, not global
undefinedundefinedalert( localSymbol.description ); // name
undefinedundefinedThere exist many "system" symbols that JavaScript uses internally, and we can use them to fine-tune various aspects of our objects.
undefinedundefinedThey are listed in the specification in the undefinedundefinedWell-known symbols table:undefinedundefined
undefinedundefinedSymbol.hasInstanceundefinedundefined
Symbol.isConcatSpreadableundefinedundefined
Symbol.iteratorundefinedundefined
Symbol.toPrimitiveundefinedundefined
For instance,
undefinedundefinedSymbol.toPrimitive allows us to
describe object to primitive conversion. We'll see its use very
soon.undefinedundefined
Other symbols will also become familiar when we study the corresponding language features.
undefinedundefinedundefinedundefinedSymbol is
a primitive type for unique identifiers.undefinedundefined
Symbols are created with
undefinedundefinedSymbol() call with an optional
description (name).undefinedundefined
Symbols are always different values, even if they have the same
name. If we want same-named symbols to be equal, then we should
use the global registry:
undefinedundefinedSymbol.for(key) returns (creates
if needed) a global symbol with
undefinedundefinedkey as the name. Multiple calls
of undefinedundefinedSymbol.for with the same
undefinedundefinedkey return exactly the same
symbol.undefinedundefined
Symbols have two main use cases:
undefinedundefined"Hidden" object
properties. If we want to add a property into an object that
"belongs" to another script or a library, we can create a
symbol and use it as a property key. A symbolic property
does not appear in undefinedundefinedfor..in,
so it won't be accidentally processed together with other
properties. Also it won't be accessed directly, because
another script does not have our symbol. So the property
will be protected from accidental use or
overwrite.undefinedundefined
So we can "covertly" hide something into objects that we need, but others should not see, using symbolic properties.
undefinedundefinedThere are many system symbols used by
JavaScript which are accessible as
undefinedundefinedSymbol.*. We can use them to
alter some built-in behaviors. For instance, later in the
tutorial we'll use
undefinedundefinedSymbol.iterator for
undefinedundefinediterables,
undefinedundefinedSymbol.toPrimitive to setup
undefinedundefinedobject-to-primitive
conversion and so on.undefinedundefined
Technically, symbols are not 100% hidden. There is a built-in method undefinedundefinedObject.getOwnPropertySymbols(obj) that allows us to get all symbols. Also there is a method named undefinedundefinedReflect.ownKeys(obj) that returns undefinedundefinedall keys of an object including symbolic ones. So they are not really hidden. But most libraries, built-in functions and syntax constructs don't use these methods.undefinedundefined
undefinedundefinedWhat happens when objects
are added undefinedundefinedobj1 + obj2, subtracted
undefinedundefinedobj1 - obj2 or printed using
undefinedundefinedalert(obj)?undefinedundefined
In that case, objects are auto-converted to primitives, and then the operation is carried out.
undefinedundefinedIn the chapter undefinedundefinedinfo:type-conversions we've seen the rules for numeric, string and boolean conversions of primitives. But we left a gap for objects. Now, as we know about methods and symbols it becomes possible to fill it.undefinedundefined
undefinedundefinedtrue in a boolean context.
There are only numeric and string
conversions.undefinedundefinedDate objects (to be covered in
the chapter undefinedundefinedinfo:date) can be subtracted, and the result
of undefinedundefineddate1 - date2 is the time
difference between two dates.undefinedundefinedalert(obj) and in similar
contexts.undefinedundefinedWe can fine-tune string and numeric conversion, using special object methods.
undefinedundefinedThere are three variants of type conversion, so-called "hints", described in the undefinedundefinedspecification:undefinedundefined
undefinedundefined"string"undefinedundefinedFor an
object-to-string conversion, when we're doing an operation
on an object that expects a string, like
undefinedundefinedalert:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// outputundefinedundefinedundefinedundefinedundefinedundefinedalert(obj)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// using object as a property keyundefinedundefinedundefinedundefinedanotherObj[obj] undefinedundefined=undefinedundefined123undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
"number"undefinedundefinedFor an object-to-number conversion, like when we're doing maths:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// explicit conversionundefinedundefinedundefinedundefinedundefinedundefinedlet num undefinedundefined=undefinedundefinedNumber(obj)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// maths (except binary plus)undefinedundefinedundefinedundefinedundefinedundefinedlet n undefinedundefined=undefinedundefined+objundefinedundefined;undefinedundefined// unary plusundefinedundefinedundefinedundefinedundefinedundefinedlet delta undefinedundefined= date1 undefinedundefined- date2undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// less/greater comparisonundefinedundefinedundefinedundefinedundefinedundefinedlet greater undefinedundefined= user1 undefinedundefined> user2undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
"default"undefinedundefined
Occurs in rare cases when the operator is "not sure" what type to expect.
undefinedundefinedFor instance, binary plus
undefinedundefined+ can work both with strings
(concatenates them) and numbers (adds them), so both strings
and numbers would do. So if a binary plus gets an object as
an argument, it uses the
undefinedundefined"default" hint to convert
it.undefinedundefined
Also, if an
object is compared using undefinedundefined==
with a string, number or a symbol, it's also unclear which
conversion should be done, so the
undefinedundefined"default" hint is
used.undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// binary plus uses the "default" hintundefinedundefinedundefinedundefinedundefinedundefinedlet total undefinedundefined= obj1 undefinedundefined+ obj2undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// obj == number uses the "default" hintundefinedundefinedundefinedundefinedundefinedundefinedif (user undefinedundefined==undefinedundefined1) undefinedundefined{ ... undefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The greater and less comparison
operators, such as
undefinedundefined<undefinedundefined>,
can work with both strings and numbers too. Still, they use
the undefinedundefined"number" hint, not
undefinedundefined"default". That's for
historical reasons.undefinedundefined
In practice though, we don't need to remember these
peculiar details, because all built-in objects except for
one case (undefinedundefinedDate object, we'll
learn it later) implement
undefinedundefined"default" conversion the same
way as undefinedundefined"number". And we can
do the same.undefinedundefined
``undefinedundefinedsmart header="No"boolean"`
hint" Please note - there are only three hints. It's that
simple.undefinedundefined
There is no
"boolean" hint (all objects are
undefinedundefinedtrue in boolean context) or
anything else. And if we treat
undefinedundefined"default" and
undefinedundefined"number" the same, like most
built-ins do, then there are only two conversions.
undefinedundefined
undefinedundefinedTo do the conversion, JavaScript tries to find and call three object methods:undefinedundefined
undefinedundefinedobj[Symbol.toPrimitive](hint) -
the method with the symbolic key
undefinedundefinedSymbol.toPrimitive (system
symbol), if such method exists,undefinedundefined"string"undefinedundefinedobj.toString() and
undefinedundefinedobj.valueOf(), whatever
exists.undefinedundefined"number" or
undefinedundefined"default"undefinedundefinedobj.valueOf() and
undefinedundefinedobj.toString(), whatever
exists.undefinedundefinedLet's start from the first method.
There's a built-in symbol named
undefinedundefinedSymbol.toPrimitive that should be
used to name the conversion method, like this:undefinedundefined
undefinedundefinedundefinedundefinedobj[undefinedundefinedSymbol.undefinedundefinedtoPrimitive] undefinedundefined=undefinedundefinedfunction(hint) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// must return a primitive valueundefinedundefinedundefinedundefinedundefinedundefined// hint = one of "string", "number", "default"undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
For instance, here
undefinedundefineduser object implements
it:undefinedundefined
run let user = { name: "John", money: 1000,
undefinedundefined
undefinedundefinedSymbol.toPrimitive {
alert(undefinedundefinedhint: ${hint}); return hint
== "string" ?
undefinedundefined{name: "${this.name}"} :
this.money; } };undefinedundefined
// conversions demo: alert(user); // hint: string -> {name: "John"} alert(+user); // hint: number -> 1000 alert(user + 500); // hint: default -> 1500
undefinedundefinedAs we can see from the code,
undefinedundefineduser becomes a self-descriptive
string or a money amount depending on the conversion. The single
method undefinedundefineduser[Symbol.toPrimitive]
handles all conversion cases.undefinedundefined
Methods
undefinedundefinedtoString and
undefinedundefinedvalueOf come from ancient times.
They are not symbols (symbols did not exist that long ago), but
rather "regular" string-named methods. They provide an
alternative "old-style" way to implement the
conversion.undefinedundefined
If there's
no undefinedundefinedSymbol.toPrimitive then
JavaScript tries to find them and try in the
order:undefinedundefined
toString -> valueOf for
"string" hint.undefinedundefinedvalueOf -> toString
otherwise.undefinedundefinedThese methods must return a primitive value.
If undefinedundefinedtoString or
undefinedundefinedvalueOf returns an object, then
it's ignored (same as if there were no
method).undefinedundefined
By default, a
plain object has following
undefinedundefinedtoString and
undefinedundefinedvalueOf
methods:undefinedundefined
toString method returns a
string
undefinedundefined"[object Object]".undefinedundefined
valueOf method returns the
object itself.undefinedundefinedHere's the demo:
undefinedundefinedrun let user = {name: "John"};
undefinedundefinedalert(user); // [object Object] alert(user.valueOf() === user); // true
undefinedundefinedSo if we try to use an object as a
string, like in an undefinedundefinedalert or so,
then by default we see
undefinedundefined[object Object].undefinedundefined
And the default
undefinedundefinedvalueOf is mentioned here only
for the sake of completeness, to avoid any confusion. As you can
see, it returns the object itself, and so is ignored. Don't ask
me why, that's for historical reasons. So we can assume it
doesn't exist.undefinedundefined
Let's implement these methods.
undefinedundefinedFor instance,
here undefinedundefineduser does the same as above
using a combination of undefinedundefinedtoString
and undefinedundefinedvalueOf instead of
undefinedundefinedSymbol.toPrimitive:undefinedundefined
run let user = { name: "John", money: 1000,
undefinedundefined// for hint="string" toString() {
return undefinedundefined{name: "${this.name}"};
},undefinedundefined
// for hint="number" or "default" valueOf() { return this.money; }
undefinedundefined};
undefinedundefinedalert(user); // toString -> {name: "John"} alert(+user); // valueOf -> 1000 alert(user + 500); // valueOf -> 1500
undefinedundefinedAs we can see, the behavior is the same
as the previous example with
undefinedundefinedSymbol.toPrimitive.undefinedundefined
Often we want a single "catch-all" place
to handle all primitive conversions. In this case, we can
implement undefinedundefinedtoString only, like
this:undefinedundefined
run let user = { name: "John",
undefinedundefinedtoString() { return this.name; } };
undefinedundefinedalert(user); // toString -> John alert(user + 500); // toString -> John500
undefinedundefinedIn the absence of
undefinedundefinedSymbol.toPrimitive and
undefinedundefinedvalueOf,
undefinedundefinedtoString will handle all
primitive conversions.undefinedundefined
The important thing to know about all primitive-conversion methods is that they do not necessarily return the "hinted" primitive.
undefinedundefinedThere is no control whether
undefinedundefinedtoString returns exactly a
string, or whether
undefinedundefinedSymbol.toPrimitive method returns
a number for a hint
undefinedundefined"number".undefinedundefined
The only mandatory thing: these methods must return a primitive, not an object.
undefinedundefined
``undefinedundefinedsmart header="Historical notes" For historical reasons, iftoStringundefinedundefinedorvalueOf`
returns an object, there's no error, but such value is ignored
(like if the method didn't exist). That's because in ancient
times there was no good "error" concept in
JavaScript.undefinedundefined
In
contrast,
undefinedundefinedSymbol.toPrimitiveundefinedundefinedmust
return a primitive, otherwise there will be an error.
undefinedundefined
As we know already, many operators and
functions perform type conversions, e.g. multiplication
undefinedundefined* converts operands to
numbers.undefinedundefined
If we pass an object as an argument, then there are two stages: 1. The object is converted to a primitive (using the rules described above). 2. If the resulting primitive isn't of the right type, it's converted.
undefinedundefinedFor instance:
undefinedundefinedrun let obj = { // toString handles all conversions in the absence of other methods toString() { return "2"; } };
undefinedundefinedalert(obj * 2); // 4, object converted to primitive "2", then multiplication made it a number
undefinedundefinedobj * 2 first
converts the object to primitive (that's a string
undefinedundefined"2").undefinedundefined"2" * 2 becomes
undefinedundefined2 * 2 (the string is converted
to number).undefinedundefinedBinary plus will concatenate strings in the same situation, as it gladly accepts a string:
undefinedundefinedrun let obj = { toString() { return "2"; } };
undefinedundefinedalert(obj + 2); // 22 ("2" + 2), conversion to primitive returned a string => concatenation
undefinedundefinedThe object-to-primitive conversion is called automatically by many built-in functions and operators that expect a primitive as a value.
undefinedundefinedThere are
3 types (hints) of it: - undefinedundefined"string"
(for undefinedundefinedalert and other operations
that need a string) - undefinedundefined"number"
(for maths) - undefinedundefined"default" (few
operators)undefinedundefined
The
specification describes explicitly which operator uses which
hint. There are very few operators that "don't know what to
expect" and use the undefinedundefined"default"
hint. Usually for built-in objects
undefinedundefined"default" hint is handled the
same way as undefinedundefined"number", so in
practice the last two are often merged
together.undefinedundefined
The conversion algorithm is:
undefinedundefinedobj[Symbol.toPrimitive](hint)
if the method exists,undefinedundefined"string"undefinedundefinedobj.toString() and
undefinedundefinedobj.valueOf(), whatever
exists.undefinedundefined"number" or
undefinedundefined"default"undefinedundefinedobj.valueOf() and
undefinedundefinedobj.toString(), whatever
exists.undefinedundefinedIn practice, it's often enough to implement
only undefinedundefinedobj.toString() as a
"catch-all" method for all conversions that return a
"human-readable" representation of an object, for logging or
debugging purposes.undefinedundefined
JavaScript allows us to work with primitives (strings, numbers, etc.) as if they were objects. They also provide methods to call as such. We will study those soon, but first we'll see how it works because, of course, primitives are not objects (and here we will make it even clearer).
undefinedundefinedLet's look at the key distinctions between primitives and objects.
undefinedundefinedA primitive
undefinedundefinedstring,
undefinedundefinednumber,
undefinedundefinedbigint,
undefinedundefinedboolean,
undefinedundefinedsymbol,
undefinedundefinednull and
undefinedundefinedundefined.undefinedundefined
An object
undefinedundefined{}, for
instance:
undefinedundefined{name: "John", age: 30}. There
are other kinds of objects in JavaScript: functions, for
example, are objects.undefinedundefinedOne of the best things about objects is that we can store a function as one of its properties.
undefinedundefinedrun let john = { name: "John", sayHi: function() { alert("Hi buddy!"); } };
undefinedundefinedjohn.sayHi(); // Hi buddy!
undefinedundefinedSo here we've made an object
undefinedundefinedjohn with the method
undefinedundefinedsayHi.undefinedundefined
Many built-in objects already exist, such as those that work with dates, errors, HTML elements, etc. They have different properties and methods.
undefinedundefinedBut, these features come with a cost!
undefinedundefinedObjects are "heavier" than primitives. They require additional resources to support the internal machinery.
undefinedundefinedHere's the paradox faced by the creator of JavaScript:
undefinedundefinedThe solution looks a little bit awkward, but here it is:
undefinedundefinedThe
"object wrappers" are different for each primitive type and are
called: undefinedundefinedString,
undefinedundefinedNumber,
undefinedundefinedBoolean and
undefinedundefinedSymbol. Thus, they provide
different sets of methods.undefinedundefined
For instance, there exists a string method
undefinedundefinedstr.toUpperCase()
that returns a capitalized
undefinedundefinedstr.undefinedundefined
Here's how it works:
undefinedundefinedrun let str = "Hello";
undefinedundefinedalert( str.toUpperCase() ); // HELLO
undefinedundefinedSimple, right? Here's what actually
happens in
undefinedundefinedstr.toUpperCase():undefinedundefined
str is a primitive. So
in the moment of accessing its property, a special object is
created that knows the value of the string, and has useful
methods, like
undefinedundefinedtoUpperCase().undefinedundefined
alert).undefinedundefinedstr
alone.undefinedundefinedSo primitives can provide methods, but they still remain lightweight.
undefinedundefinedThe JavaScript engine highly optimizes this process. It may even skip the creation of the extra object at all. But it must still adhere to the specification and behave as if it creates one.
undefinedundefinedA number has methods of its own, for instance, undefinedundefinedtoFixed(n) rounds the number to the given precision:undefinedundefined
undefinedundefinedrun let n = 1.23456;
undefinedundefinedalert( n.toFixed(2) ); // 1.23
undefinedundefinedWe'll see more specific methods in chapters undefinedundefinedinfo:number and undefinedundefinedinfo:string.undefinedundefined
undefinedundefined
undefinedundefinedwarn header="ConstructorsString/Number/Booleanundefinedundefinedare for internal use only" Some languages like Java allow us to explicitly create "wrapper objects" for primitives using a syntax likenew
Number(1)undefinedundefinedornew
Boolean(false)`.undefinedundefined
In JavaScript, that's also possible for historical reasons, but highly undefinedundefinedunrecommended. Things will go crazy in several places.undefinedundefined
undefinedundefinedFor instance:
undefinedundefinedrun alert( typeof 0 ); // "number"
undefinedundefinedalert( typeof new Number(0) ); // "object"!
undefinedundefinedObjects are always truthy in
undefinedundefinedif, so here the alert will show
up:undefinedundefined
run let zero = new Number(0);
undefinedundefinedif (zero) { // zero is true, because it's an object alert( "zero is truthy!?!" ); }
undefinedundefinedOn the other hand, using the same
functions undefinedundefinedString/Number/Boolean
without undefinedundefinednew is a totally sane and
useful thing. They convert a value to the corresponding type: to
a string, a number, or a boolean (primitive).undefinedundefined
For example, this is entirely valid:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet num undefinedundefined=undefinedundefinedNumber(undefinedundefined"123")undefinedundefined;undefinedundefined// convert a string to numberundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefined
undefinedundefinedwarn header="null/undefined have no methods" The special primitivesnullundefinedundefinedandundefined`
are exceptions. They have no corresponding "wrapper objects" and
provide no methods. In a sense, they are "the most
primitive".undefinedundefined
An attempt to access a property of such value would give the error:
undefinedundefinedrun alert(null.test); // error
undefinedundefinednull and
undefinedundefinedundefined provide many helpful
methods. We will study those in the upcoming
chapters.undefinedundefinedIn modern JavaScript, there are two types of numbers:
undefinedundefinedRegular numbers in JavaScript are stored in 64-bit format undefinedundefinedIEEE-754, also known as "double precision floating point numbers". These are numbers that we're using most of the time, and we'll talk about them in this chapter.undefinedundefined
undefinedundefinedBigInt numbers, to represent integers of
arbitrary length. They are sometimes needed, because a
regular number can't exceed
undefinedundefined2undefinedundefined53undefinedundefined
or be less than
undefinedundefined-2undefinedundefined53undefinedundefined.
As bigints are used in few special areas, we devote them a
special chapter undefinedundefinedinfo:bigint.undefinedundefined
So here we'll talk about regular numbers. Let's expand our knowledge of them.
undefinedundefinedImagine we need to write 1 billion. The obvious way is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet billion undefinedundefined=undefinedundefined1000000000undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
We also can use underscore
undefinedundefined_ as the
separator:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet billion undefinedundefined=undefinedundefined1_000_000_000undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Here the underscore
undefinedundefined_ plays the role of the
"syntactic sugar", it makes the number more readable. The
JavaScript engine simply ignores
undefinedundefined_ between digits, so it's exactly
the same one billion as above.undefinedundefined
In real life though, we try to avoid writing
long sequences of zeroes. We're too lazy for that. We'll try to
write something like undefinedundefined"1bn" for a
billion or undefinedundefined"7.3bn" for 7 billion
300 million. The same is true for most large
numbers.undefinedundefined
In
JavaScript, we can shorten a number by appending the letter
undefinedundefined"e" to it and specifying the
zeroes count:undefinedundefined
run let billion = 1e9; // 1 billion, literally: 1 and 9 zeroes
undefinedundefinedalert( 7.3e9 ); // 7.3 billions (same as 7300000000 or 7_300_000_000)
undefinedundefinedIn other words,
undefinedundefinede multiplies the number by
undefinedundefined1 with the given zeroes
count.undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined1e3undefinedundefined=undefinedundefined1undefinedundefined*undefinedundefined1000undefinedundefined// e3 means *1000undefinedundefinedundefinedundefinedundefinedundefined1.23e6undefinedundefined=undefinedundefined1.23undefinedundefined*undefinedundefined1000000undefinedundefined// e6 means *1000000undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Now let's write something very small. Say, 1 microsecond (one millionth of a second):
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet ms undefinedundefined=undefinedundefined0.000001undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Just like before, using
undefinedundefined"e" can help. If we'd like to
avoid writing the zeroes explicitly, we could say the same
as:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet ms undefinedundefined=undefinedundefined1e-6undefinedundefined;undefinedundefined// six zeroes to the left from 1undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
If we count the zeroes in
undefinedundefined0.000001, there are 6 of them. So
naturally it's
undefinedundefined1e-6.undefinedundefined
In other words, a negative number after
undefinedundefined"e" means a division by 1 with
the given number of zeroes:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// -3 divides by 1 with 3 zeroesundefinedundefinedundefinedundefinedundefinedundefined1e-3undefinedundefined=undefinedundefined1 / undefinedundefined1000 (undefinedundefined=undefinedundefined0.001)undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// -6 divides by 1 with 6 zeroesundefinedundefinedundefinedundefinedundefinedundefined1.23e-6undefinedundefined=undefinedundefined1.23 / undefinedundefined1000000 (undefinedundefined=undefinedundefined0.00000123)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedHexadecimal
numbers are widely used in JavaScript to represent colors,
encode characters, and for many other things. So naturally,
there exists a shorter way to write them:
undefinedundefined0x and then the
number.undefinedundefined
For instance:
undefinedundefined
undefinedundefinedjs run alert( 0xff ); // 255 alert( 0xFF ); // 255 (the same, case doesn't matter)undefinedundefined
Binary and octal numeral systems are
rarely used, but also supported using the
undefinedundefined0b and
undefinedundefined0o prefixes:undefinedundefined
run let a = 0b11111111; // binary form of 255 let b = 0o377; // octal form of 255
undefinedundefinedalert( a == b ); // true, the same number 255 at both sides
undefinedundefinedThere are only 3 numeral systems with
such support. For other numeral systems, we should use the
function undefinedundefinedparseInt (which we will
see later in this chapter).undefinedundefined
The method
undefinedundefinednum.toString(base) returns a
string representation of undefinedundefinednum in
the numeral system with the given
undefinedundefinedbase.undefinedundefined
For example: run let num = 255;
undefinedundefinedalert( num.toString(16) ); // ff alert( num.toString(2) ); // 11111111
undefinedundefinedThe undefinedundefinedbase
can vary from undefinedundefined2 to
undefinedundefined36. By default it's
undefinedundefined10.undefinedundefined
Common use cases for this are:
undefinedundefined0..9 or
undefinedundefinedA..F.undefinedundefined0 or
undefinedundefined1.undefinedundefined
undefinedundefinedbase=36 is the maximum,
digits can be undefinedundefined0..9 or
undefinedundefinedA..Z. The whole latin
alphabet is used to represent a number. A funny, but useful
case for undefinedundefined36 is when we need
to turn a long numeric identifier into something shorter,
for example to make a short url. Can simply represent it in
the numeral system with base
undefinedundefined36:undefinedundefined
undefinedundefinedjs run alert( 123456..toString(36) ); // 2n9cundefinedundefined
``undefinedundefinedwarn header="Two dots to call a method" Please note that two dots in123456..toString(36)undefinedundefinedis not a typo. If we want to call a method directly on a number, liketoStringundefinedundefinedin the example above, then we need to place two dots..`
after it.undefinedundefined
If we placed
a single dot:
undefinedundefined123456.toString(36), then there
would be an error, because JavaScript syntax implies the decimal
part after the first dot. And if we place one more dot, then
JavaScript knows that the decimal part is empty and now goes the
method.undefinedundefined
Also could
write undefinedundefined(123456).toString(36).
undefinedundefined
One of the most used operations when working with numbers is rounding.
undefinedundefinedThere are several built-in functions for rounding:
undefinedundefinedMath.floorundefinedundefined
3.1 becomes
undefinedundefined3, and
undefinedundefined-1.1 becomes
undefinedundefined-2.
undefinedundefinedMath.ceilundefinedundefined
3.1 becomes
undefinedundefined4, and
undefinedundefined-1.1 becomes
undefinedundefined-1.
undefinedundefinedMath.roundundefinedundefined
3.1 becomes
undefinedundefined3,
undefinedundefined3.6 becomes
undefinedundefined4, the middle case:
undefinedundefined3.5 rounds up to
undefinedundefined4 too.
undefinedundefinedMath.trunc (not supported by
Internet Explorer)undefinedundefined3.1 becomes
undefinedundefined3,
undefinedundefined-1.1 becomes
undefinedundefined-1.
undefinedundefinedHere's the table to summarize the differences between them:
undefinedundefined| undefinedundefined |
undefinedundefinedMath.floorundefinedundefined
| undefinedundefined
undefinedundefinedMath.ceilundefinedundefined
| undefinedundefined
undefinedundefinedMath.roundundefinedundefined
| undefinedundefined
undefinedundefinedMath.truncundefinedundefined
| undefinedundefined
|---|---|---|---|---|
undefinedundefined3.1undefinedundefined |
undefinedundefined
undefinedundefined3undefinedundefined |
undefinedundefined
undefinedundefined4undefinedundefined |
undefinedundefined
undefinedundefined3undefinedundefined |
undefinedundefined
undefinedundefined3undefinedundefined |
undefinedundefined
undefinedundefined3.6undefinedundefined |
undefinedundefined
undefinedundefined3undefinedundefined |
undefinedundefined
undefinedundefined4undefinedundefined |
undefinedundefined
undefinedundefined4undefinedundefined |
undefinedundefined
undefinedundefined3undefinedundefined |
undefinedundefined
undefinedundefined-1.1undefinedundefined |
undefinedundefined
undefinedundefined-2undefinedundefined |
undefinedundefined
undefinedundefined-1undefinedundefined |
undefinedundefined
undefinedundefined-1undefinedundefined |
undefinedundefined
undefinedundefined-1undefinedundefined |
undefinedundefined
undefinedundefined-1.6undefinedundefined |
undefinedundefined
undefinedundefined-2undefinedundefined |
undefinedundefined
undefinedundefined-1undefinedundefined |
undefinedundefined
undefinedundefined-2undefinedundefined |
undefinedundefined
undefinedundefined-1undefinedundefined |
undefinedundefined
These functions
cover all of the possible ways to deal with the decimal part of
a number. But what if we'd like to round the number to
undefinedundefinedn-th digit after the
decimal?undefinedundefined
For instance,
we have undefinedundefined1.2345 and want to round
it to 2 digits, getting only
undefinedundefined1.23.undefinedundefined
There are two ways to do so:
undefinedundefinedMultiply-and-divide.
undefinedundefinedFor example, to round the number to the
2nd digit after the decimal, we can multiply the number by
undefinedundefined100 (or a bigger power of
10), call the rounding function and then divide it back. run
let num = 1.23456;undefinedundefined
alert( Math.round(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
undefinedundefinedThe method undefinedundefinedtoFixed(n)
rounds the number to undefinedundefinedn digits
after the point and returns a string representation of the
result.undefinedundefined
undefinedundefinedjs run let num = 12.34; alert( num.toFixed(1) ); // "12.3"undefinedundefined
This rounds up or down to the nearest
value, similar to
undefinedundefinedMath.round:undefinedundefined
undefinedundefinedjs run let num = 12.36; alert( num.toFixed(1) ); // "12.4"undefinedundefined
Please note that result of
undefinedundefinedtoFixed is a string. If the
decimal part is shorter than required, zeroes are appended
to the end:undefinedundefined
undefinedundefinedjs run let num = 12.34; alert( num.toFixed(5) ); // "12.34000", added zeroes to make exactly 5 digitsundefinedundefined
We can convert it to a number using
the unary plus or a undefinedundefinedNumber()
call:
undefinedundefined+num.toFixed(5).undefinedundefined
Internally, a number is represented in 64-bit format undefinedundefinedIEEE-754, so there are exactly 64 bits to store a number: 52 of them are used to store the digits, 11 of them store the position of the decimal point (they are zero for integer numbers), and 1 bit is for the sign.undefinedundefined
undefinedundefinedIf a number is too big, it would overflow the 64-bit storage, potentially giving an infinity:
undefinedundefined
undefinedundefinedjs run alert( 1e500 ); // Infinityundefinedundefined
What may be a little less obvious, but happens quite often, is the loss of precision.
undefinedundefinedConsider this (falsy!) test:
undefinedundefined
undefinedundefinedjs run alert( 0.1 + 0.2 == 0.3 ); // *!*false*/!*undefinedundefined
That's right, if we check whether the sum
of undefinedundefined0.1 and
undefinedundefined0.2 is
undefinedundefined0.3, we get
undefinedundefinedfalse.undefinedundefined
Strange! What is it then if not
undefinedundefined0.3?undefinedundefined
undefinedundefinedjs run alert( 0.1 + 0.2 ); // 0.30000000000000004undefinedundefined
Ouch! There are more consequences than an
incorrect comparison here. Imagine you're making an e-shopping
site and the visitor puts undefinedundefined$0.10
and undefinedundefined$0.20 goods into their cart.
The order total will be
undefinedundefined$0.30000000000000004. That would
surprise anyone.undefinedundefined
But why does this happen?
undefinedundefinedA number is stored
in memory in its binary form, a sequence of bits - ones and
zeroes. But fractions like undefinedundefined0.1,
undefinedundefined0.2 that look simple in the
decimal numeric system are actually unending fractions in their
binary form.undefinedundefined
In other
words, what is undefinedundefined0.1? It is one
divided by ten undefinedundefined1/10, one-tenth.
In decimal numeral system such numbers are easily representable.
Compare it to one-third: undefinedundefined1/3. It
becomes an endless fraction
undefinedundefined0.33333(3).undefinedundefined
So, division by powers
undefinedundefined10 is guaranteed to work well in
the decimal system, but division by
undefinedundefined3 is not. For the same reason, in
the binary numeral system, the division by powers of
undefinedundefined2 is guaranteed to work, but
undefinedundefined1/10 becomes an endless binary
fraction.undefinedundefined
There's just no way to store undefinedundefinedexactly 0.1 or undefinedundefinedexactly 0.2 using the binary system, just like there is no way to store one-third as a decimal fraction.undefinedundefined
undefinedundefinedThe numeric format IEEE-754 solves this by rounding to the nearest possible number. These rounding rules normally don't allow us to see that "tiny precision loss", but it exists.
undefinedundefinedWe
can see this in action:
undefinedundefinedjs run alert( 0.1.toFixed(20) ); // 0.10000000000000000555undefinedundefined
And when we sum two numbers, their "precision losses" add up.
undefinedundefinedThat's why
undefinedundefined0.1 + 0.2 is not exactly
undefinedundefined0.3.undefinedundefined
smart header="Not only JavaScript" The same issue exists in many other programming languages.
undefinedundefinedPHP, Java, C, Perl, Ruby give exactly the same result, because they are based on the same numeric format.
undefinedundefinedCan we work around the problem? Sure, the most reliable method is to round the result with the help of a method undefinedundefinedtoFixed(n):undefinedundefined
undefinedundefined
undefinedundefinedjs run let sum = 0.1 + 0.2; alert( sum.toFixed(2) ); // 0.30undefinedundefined
Please note that
undefinedundefinedtoFixed always returns a string.
It ensures that it has 2 digits after the decimal point. That's
actually convenient if we have an e-shopping and need to show
undefinedundefined$0.30. For other cases, we can
use the unary plus to coerce it into a number:undefinedundefined
undefinedundefinedjs run let sum = 0.1 + 0.2; alert( +sum.toFixed(2) ); // 0.3undefinedundefined
We also can temporarily multiply the numbers by 100 (or a bigger number) to turn them into integers, do the maths, and then divide back. Then, as we're doing maths with integers, the error somewhat decreases, but we still get it on division:
undefinedundefined
undefinedundefinedjs run alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3 alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001undefinedundefined
So, multiply/divide approach reduces the error, but doesn't remove it totally.
undefinedundefinedSometimes we could try to evade fractions at all. Like if we're dealing with a shop, then we can store prices in cents instead of dollars. But what if we apply a discount of 30%? In practice, totally evading fractions is rarely possible. Just round them to cut "tails" when needed.
undefinedundefinedsmart header="The funny thing" Try running this:
undefinedundefined
undefinedundefinedjs run // Hello! I'm a self-increasing number! alert( 9999999999999999 ); // shows 10000000000000000undefinedundefined
This suffers from the same issue: a loss of precision. There are 64 bits for the number, 52 of them can be used to store digits, but that's not enough. So the least significant digits disappear.
undefinedundefinedJavaScript doesn't trigger an error in such events. It does its best to fit the number into the desired format, but unfortunately, this format is not big enough.
undefinedundefined
``undefinedundefinedsmart header="Two zeroes" Another funny consequence of the internal representation of numbers is the existence of two zeroes:0undefinedundefinedand-0`.undefinedundefined
That's because a sign is represented by a single bit, so it can be set or not set for any number including a zero.
undefinedundefinedIn most cases the distinction is unnoticeable, because operators are suited to treat them as the same.
undefinedundefinedRemember these two special numeric values?
undefinedundefinedInfinity
(and undefinedundefined-Infinity) is a special
numeric value that is greater (less) than
anything.undefinedundefinedNaN represents an
error.undefinedundefinedThey belong to the type
undefinedundefinednumber, but are not "normal"
numbers, so there are special functions to check for
them:undefinedundefined
undefinedundefinedisNaN(value) converts its
argument to a number and then tests it for being
undefinedundefinedNaN:undefinedundefined
undefinedundefinedjs run alert( isNaN(NaN) ); // true alert( isNaN("str") ); // trueundefinedundefined
But do we need this function? Can't
we just use the comparison
undefinedundefined=== NaN? Sorry, but the
answer is no. The value undefinedundefinedNaN
is unique in that it does not equal anything, including
itself:undefinedundefined
undefinedundefinedjs run alert( NaN === NaN ); // falseundefinedundefined
undefinedundefinedisFinite(value) converts its
argument to a number and returns
undefinedundefinedtrue if it's a regular
number, not
undefinedundefinedNaN/Infinity/-Infinity:undefinedundefined
undefinedundefinedjs run alert( isFinite("15") ); // true alert( isFinite("str") ); // false, because a special value: NaN alert( isFinite(Infinity) ); // false, because a special value: Infinityundefinedundefined
Sometimes
undefinedundefinedisFinite is used to validate
whether a string value is a regular number:undefinedundefined
run let num = +prompt("Enter a number", '');
undefinedundefined// will be true unless you enter Infinity, -Infinity or not a number alert( isFinite(num) );
undefinedundefinedPlease note that an empty or a space-only
string is treated as undefinedundefined0 in all
numeric functions including
undefinedundefinedisFinite.undefinedundefined
``undefinedundefinedsmart header="Compare withObject.is`"undefinedundefined
There is a special built-in method
undefinedundefinedundefinedundefinedObject.isundefinedundefined
that compares values like undefinedundefined===,
but is more reliable for two edge cases:undefinedundefined
NaN:
undefinedundefinedObject.is(NaN, NaN) === true,
that's a good thing.undefinedundefined0 and
undefinedundefined-0 are different:
undefinedundefinedObject.is(0, -0) === false,
technically that's true, because internally the number has a
sign bit that may be different even if all other bits are
zeroes.undefinedundefinedIn all other cases,
undefinedundefinedObject.is(a, b) is the same as
undefinedundefineda === b.undefinedundefined
This way of comparison is often used in
JavaScript specification. When an internal algorithm needs to
compare two values for being exactly the same, it uses
undefinedundefinedObject.is (internally called
undefinedundefinedSameValue).
undefinedundefined
Numeric conversion using a plus
undefinedundefined+ or
undefinedundefinedNumber() is strict. If a value is
not exactly a number, it fails:undefinedundefined
undefinedundefinedjs run alert( +"100px" ); // NaNundefinedundefined
The sole exception is spaces at the beginning or at the end of the string, as they are ignored.
undefinedundefinedBut in real life we often have values in
units, like undefinedundefined"100px" or
undefinedundefined"12pt" in CSS. Also in many
countries the currency symbol goes after the amount, so we have
undefinedundefined"19€" and would like to extract a
numeric value out of that.undefinedundefined
That's what
undefinedundefinedparseInt and
undefinedundefinedparseFloat are
for.undefinedundefined
They "read" a
number from a string until they can't. In case of an error, the
gathered number is returned. The function
undefinedundefinedparseInt returns an integer,
whilst undefinedundefinedparseFloat will return a
floating-point number:undefinedundefined
run alert( parseInt(‘100px') ); // 100 alert( parseFloat(‘12.5em') ); // 12.5
undefinedundefinedalert( parseInt(‘12.3') ); // 12, only the integer part is returned alert( parseFloat(‘12.3.4') ); // 12.3, the second point stops the reading
undefinedundefinedThere are situations when
undefinedundefinedparseInt/parseFloat will return
undefinedundefinedNaN. It happens when no digits
could be read:undefinedundefined
undefinedundefinedjs run alert( parseInt('a123') ); // NaN, the first symbol stops the processundefinedundefined
undefinedundefinedsmart header="The second argument ofparseInt(str,
radix)undefinedundefined" TheparseInt()undefinedundefinedfunction has an optional second parameter. It specifies the base of the numeral system, soparseInt`
can also parse strings of hex numbers, binary numbers and so
on:undefinedundefined
run alert( parseInt(‘0xff', 16) ); // 255 alert( parseInt(‘ff', 16) ); // 255, without 0x also works
undefinedundefinedalert( parseInt(‘2n9c', 36) ); // 123456
undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefinedJavaScript has a built-in undefinedundefinedMath object which contains a small library of mathematical functions and constants.undefinedundefined
undefinedundefinedA few examples:
undefinedundefinedMath.random()undefinedundefined
Returns a random number from 0 to 1 (not including 1).
undefinedundefined
undefinedundefinedjs run alert( Math.random() ); // 0.1234567894322 alert( Math.random() ); // 0.5435252343232 alert( Math.random() ); // ... (any random numbers)undefinedundefined
Math.max(a, b, c...) /
undefinedundefinedMath.min(a, b, c...)undefinedundefined
Returns the greatest/smallest from the arbitrary number of arguments.
undefinedundefined
undefinedundefinedjs run alert( Math.max(3, 5, -10, 0, 1) ); // 5 alert( Math.min(1, 2) ); // 1undefinedundefined
Math.pow(n, power)undefinedundefined
Returns
undefinedundefinedn raised to the given
power.undefinedundefined
undefinedundefinedjs run alert( Math.pow(2, 10) ); // 2 in power 10 = 1024undefinedundefined
There are more functions and constants in
undefinedundefinedMath object, including
trigonometry, which you can find in the undefinedundefineddocs
for the Math object.undefinedundefined
To write numbers with many zeroes:
undefinedundefined"e"
with the zeroes count to the number. Like:
undefinedundefined123e6 is the same as
undefinedundefined123 with 6 zeroes
undefinedundefined123000000.undefinedundefined
"e" causes the number to be
divided by 1 with given zeroes. E.g.
undefinedundefined123e-6 means
undefinedundefined0.000123
(undefinedundefined123
millionths).undefinedundefinedFor different numeral systems:
undefinedundefined0x), octal
(undefinedundefined0o) and binary
(undefinedundefined0b) systems.undefinedundefined
parseInt(str, base) parses the
string undefinedundefinedstr into an integer in
numeral system with given undefinedundefinedbase,
undefinedundefined2 ≤ base ≤ 36.undefinedundefined
num.toString(base) converts a
number to a string in the numeral system with the given
undefinedundefinedbase.undefinedundefinedFor converting
values like undefinedundefined12pt and
undefinedundefined100px to a
number:undefinedundefined
parseInt/parseFloat for the
"soft" conversion, which reads a number from a string and then
returns the value they could read before the
error.undefinedundefinedFor fractions:
undefinedundefinedMath.floor,
undefinedundefinedMath.ceil,
undefinedundefinedMath.trunc,
undefinedundefinedMath.round or
undefinedundefinednum.toFixed(precision).undefinedundefined
More mathematical functions:
undefinedundefinedIn JavaScript, the textual data is stored as strings. There is no separate type for a single character.
undefinedundefinedThe internal format for strings is always undefinedundefinedUTF-16, it is not tied to the page encoding.undefinedundefined
undefinedundefinedLet's recall the kinds of quotes.
undefinedundefinedStrings can be enclosed within either single quotes, double quotes or backticks:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet single undefinedundefined=undefinedundefined'single-quoted'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedlet double undefinedundefined=undefinedundefined"double-quoted"undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet backticks undefinedundefined=undefinedundefined`backticks`undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Single and double quotes are
essentially the same. Backticks, however, allow us to embed any
expression into the string, by wrapping it in
undefinedundefined${…}:undefinedundefined
run function sum(a, b) { return a + b; }
undefinedundefined
alert(undefinedundefined1 + 2 = ${sum(1, 2)}.); //
1 + 2 = 3.
undefinedundefined
Another advantage of using backticks is that they allow a string to span multiple lines:
undefinedundefined
``undefinedundefinedjs run let guestList =Guests: *
John * Pete * Mary `;undefinedundefined
alert(guestList); // a list of guests, multiple lines
undefinedundefinedLooks natural, right? But single or double quotes do not work this way.
undefinedundefinedIf we use them and try to use multiple lines, there'll be an error:
undefinedundefined
undefinedundefinedjs run let guestList = "Guests: // Error: Unexpected token ILLEGAL * John";undefinedundefined
Single and double quotes come from ancient times of language creation when the need for multiline strings was not taken into account. Backticks appeared much later and thus are more versatile.
undefinedundefined
Backticks also allow us to specify a "template function" before
the first backtick. The syntax is:
undefinedundefinedfunc`string`. The function
undefinedundefinedfunc is called automatically,
receives the string and embedded expressions and can process
them. This is called "tagged templates". This feature makes it
easier to implement custom templating, but is rarely used in
practice. You can read more about it in the undefinedundefinedmanual.undefinedundefined
It is still possible to
create multiline strings with single and double quotes by using
a so-called "newline character", written as
undefinedundefined\n, which denotes a line
break:undefinedundefined
run let guestList = "Guests:JohnPeteMary";
undefinedundefinedalert(guestList); // a multiline list of guests
undefinedundefinedFor example, these two lines are equal, just written differently:
undefinedundefinedrun let str1 = "Hello"; // two lines using a "newline symbol"
undefinedundefined// two lines using a normal newline and
backticks let str2 =
undefinedundefinedHello World;undefinedundefined
alert(str1 == str2); // true
undefinedundefinedThere are other, less common "special" characters.
undefinedundefinedHere's the full list:
undefinedundefined| Character | undefinedundefinedDescription | undefinedundefined
|---|---|
undefinedundefined\nundefinedundefined |
undefinedundefinedNew line | undefinedundefined
undefinedundefined\rundefinedundefined |
undefinedundefinedCarriage return: not used alone.
Windows text files use a combination of two characters
undefinedundefined\r\n to represent a line
break.undefinedundefined | undefinedundefined
undefinedundefined\',
undefinedundefined\"undefinedundefined |
undefinedundefinedQuotes | undefinedundefined
undefinedundefined\\undefinedundefined |
undefinedundefinedBackslash | undefinedundefined
undefinedundefined\tundefinedundefined |
undefinedundefinedTab | undefinedundefined
undefinedundefined\b,
undefinedundefined\f,
undefinedundefined\vundefinedundefined |
undefinedundefinedBackspace, Form Feed, Vertical Tab - kept for compatibility, not used nowadays. | undefinedundefined
undefinedundefined\xXXundefinedundefined |
undefinedundefinedUnicode character with the given
hexadecimal Unicode undefinedundefinedXX,
e.g. undefinedundefined'\x7A' is the same as
undefinedundefined'z'.undefinedundefined |
undefinedundefined
undefinedundefined\uXXXXundefinedundefined
| undefinedundefinedA Unicode symbol with the hex
code undefinedundefinedXXXX in UTF-16
encoding, for instance
undefinedundefined\u00A9 - is a Unicode for
the copyright symbol undefinedundefined©. It
must be exactly 4 hex digits.undefinedundefined |
undefinedundefined
undefinedundefined\u{X…XXXXXX} (1 to 6 hex
characters)undefinedundefined | undefinedundefinedA Unicode symbol with the given UTF-32 encoding. Some rare characters are encoded with two Unicode symbols, taking 4 bytes. This way we can insert long codes. | undefinedundefined
Examples with Unicode:
undefinedundefined
undefinedundefinedjs run alert( "\u00A9" ); // © alert( "\u{20331}" ); // 佫, a rare Chinese hieroglyph (long Unicode) alert( "\u{1F60D}" ); // 😍, a smiling face symbol (another long Unicode)undefinedundefined
All special characters start with a
backslash character undefinedundefined\. It is also
called an "escape character".undefinedundefined
We might also use it if we wanted to insert a quote into the string.
undefinedundefinedFor instance:
undefinedundefined
undefinedundefinedjs run alert( 'I*!*\'*/!*m the Walrus!' ); // *!*I'm*/!* the Walrus!undefinedundefined
As you can see, we have to prepend the
inner quote by the backslash undefinedundefined\',
because otherwise it would indicate the string
end.undefinedundefined
Of course, only the quotes that are the same as the enclosing ones need to be escaped. So, as a more elegant solution, we could switch to double quotes or backticks instead:
undefinedundefined
undefinedundefinedjs run alert( `I'm the Walrus!` ); // I'm the Walrus!undefinedundefined
Note that the backslash
undefinedundefined\ serves for the correct reading
of the string by JavaScript, then disappears. The in-memory
string has no undefinedundefined\. You can clearly
see that in undefinedundefinedalert from the
examples above.undefinedundefined
But
what if we need to show an actual backslash
undefinedundefined\ within the
string?undefinedundefined
That's
possible, but we need to double it like
undefinedundefined\\:undefinedundefined
undefinedundefinedjs run alert( `The backslash: \\` ); // The backslash: \undefinedundefined
The undefinedundefinedlength
property has the string length:undefinedundefined
undefinedundefinedjs run alert( `My\n`.length ); // 3undefinedundefined
Note that
undefinedundefined\n is a single "special"
character, so the length is indeed
undefinedundefined3.undefinedundefined
``undefinedundefinedwarn header="lengthundefinedundefinedis a property" People with a background in some other languages sometimes mistype by callingstr.length()undefinedundefinedinstead of juststr.length`.
That doesn't work.undefinedundefined
Please note that undefinedundefinedstr.length is a
numeric property, not a function. There is no need to add
parenthesis after it.
undefinedundefined
To get a character at position
undefinedundefinedpos, use square brackets
undefinedundefined[pos] or call the method
undefinedundefinedstr.charAt(pos). The first
character starts from the zero position:undefinedundefined
``undefinedundefinedjs run let str =Hello`;undefinedundefined
// the first character alert( str[0] ); // H alert( str.charAt(0) ); // H
undefinedundefined// the last character alert( str[str.length - 1] ); // o
undefinedundefinedThe square brackets are a modern way of
getting a character, while undefinedundefinedcharAt
exists mostly for historical reasons.undefinedundefined
The only difference between them is that if
no character is found, undefinedundefined[] returns
undefinedundefinedundefined, and
undefinedundefinedcharAt returns an empty
string:undefinedundefined
``undefinedundefinedjs run let str =Hello`;undefinedundefined
alert( str[1000] ); // undefined alert( str.charAt(1000) ); // '' (an empty string)
undefinedundefinedWe can also iterate over characters using
undefinedundefinedfor..of:undefinedundefined
undefinedundefinedjs run for (let char of "Hello") { alert(char); // H,e,l,l,o (char becomes "H", then "e", then "l" etc) }undefinedundefined
Strings can't be changed in JavaScript. It is impossible to change a character.
undefinedundefinedLet's try it to show that it doesn't work:
undefinedundefinedrun let str = ‘Hi';
undefinedundefinedstr[0] = ‘h'; // error alert( str[0] ); // doesn't work
undefinedundefinedThe usual workaround is to create a whole
new string and assign it to undefinedundefinedstr
instead of the old one.undefinedundefined
For instance:
undefinedundefinedrun let str = ‘Hi';
undefinedundefinedstr = ‘h' + str[1]; // replace the string
undefinedundefinedalert( str ); // hi
undefinedundefinedIn the following sections we'll see more examples of this.
undefinedundefinedMethods undefinedundefinedtoLowerCase() and undefinedundefinedtoUpperCase() change the case:undefinedundefined
undefinedundefined
undefinedundefinedjs run alert( 'Interface'.toUpperCase() ); // INTERFACE alert( 'Interface'.toLowerCase() ); // interfaceundefinedundefined
Or, if we want a single character lowercased:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefined'Interface'[undefinedundefined0].undefinedundefinedtoLowerCase() )undefinedundefined;undefinedundefined// 'i'undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
There are multiple ways to look for a substring within a string.
undefinedundefinedThe first method is undefinedundefinedstr.indexOf(substr, pos).undefinedundefined
undefinedundefinedIt looks
for the undefinedundefinedsubstr in
undefinedundefinedstr, starting from the given
position undefinedundefinedpos, and returns the
position where the match was found or
undefinedundefined-1 if nothing can be
found.undefinedundefined
For instance:
undefinedundefinedrun let str = ‘Widget with id';
undefinedundefinedalert( str.indexOf(‘Widget') ); // 0, because ‘Widget' is found at the beginning alert( str.indexOf(‘widget') ); // -1, not found, the search is case-sensitive
undefinedundefinedalert( str.indexOf("id") ); // 1, "id" is found at the position 1 (..idget with id)
undefinedundefinedThe optional second parameter allows us to start searching from a given position.
undefinedundefinedFor instance, the first occurrence of
undefinedundefined"id" is at position
undefinedundefined1. To look for the next
occurrence, let's start the search from position
undefinedundefined2:undefinedundefined
run let str = ‘Widget with id';
undefinedundefinedalert( str.indexOf(‘id', 2) ) // 12
undefinedundefinedIf we're interested in all occurrences,
we can run undefinedundefinedindexOf in a loop.
Every new call is made with the position after the previous
match:undefinedundefined
run let str = ‘As sly as a fox, as strong as an ox';
undefinedundefinedlet target = ‘as'; // let's look for it
undefinedundefinedlet pos = 0; while (true) { let foundPos = str.indexOf(target, pos); if (foundPos == -1) break;
undefinedundefinedalert(
undefinedundefinedFound at ${foundPos} ); pos =
foundPos + 1; // continue the search from the next position }
undefinedundefined
The same algorithm can be layed out shorter:
undefinedundefinedrun let str = "As sly as a fox, as strong as an ox"; let target = "as";
undefinedundefinedundefinedundefined! let pos = -1; while ((pos = str.indexOf(target, pos + 1)) != -1) { alert( pos ); } undefinedundefined/! undefinedundefined
undefinedundefined
``undefinedundefinedsmart header="str.lastIndexOf(substr,
position)`" There is also a similar method undefinedundefinedstr.lastIndexOf(substr,
position) that searches from the end of a string to its
beginning.undefinedundefined
It would list the occurrences in the reverse order.
undefinedundefinedThere is a slight inconvenience with
undefinedundefinedindexOf in the
undefinedundefinedif test. We can't put it in the
undefinedundefinedif like this:undefinedundefined
run let str = "Widget with id";
undefinedundefinedif (str.indexOf("Widget")) { alert("We found it"); // doesn't work! }
undefinedundefinedThe undefinedundefinedalert
in the example above doesn't show because
undefinedundefinedstr.indexOf("Widget") returns
undefinedundefined0 (meaning that it found the
match at the starting position). Right, but
undefinedundefinedif considers
undefinedundefined0 to be
undefinedundefinedfalse.undefinedundefined
So, we should actually check for
undefinedundefined-1, like this:undefinedundefined
run let str = "Widget with id";
undefinedundefinedundefinedundefined! if (str.indexOf("Widget") != -1) { undefinedundefined/! alert("We found it"); // works now! } undefinedundefined
undefinedundefinedOne of the old tricks used here is the
undefinedundefinedbitwise
NOTundefinedundefined~ operator. It converts
the number to a 32-bit integer (removes the decimal part if
exists) and then reverses all bits in its binary
representation.undefinedundefined
In
practice, that means a simple thing: for 32-bit integers
undefinedundefined~n equals
undefinedundefined-(n+1).undefinedundefined
For instance:
undefinedundefined
undefinedundefinedjs run alert( ~2 ); // -3, the same as -(2+1) alert( ~1 ); // -2, the same as -(1+1) alert( ~0 ); // -1, the same as -(0+1) *!* alert( ~-1 ); // 0, the same as -(-1+1) */!*undefinedundefined
As we can see,
undefinedundefined~n is zero only if
undefinedundefinedn == -1 (that's for any 32-bit
signed integer
undefinedundefinedn).undefinedundefined
So, the test
undefinedundefinedif ( ~str.indexOf("...") ) is
truthy only if the result of
undefinedundefinedindexOf is not
undefinedundefined-1. In other words, when there is
a match.undefinedundefined
People use it
to shorten undefinedundefinedindexOf
checks:undefinedundefined
run let str = "Widget";
undefinedundefinedif (~str.indexOf("Widget")) { alert( ‘Found it!''' ); // works }
undefinedundefinedIt is usually not recommended to use language features in a non-obvious way, but this particular trick is widely used in old code, so we should understand it.
undefinedundefinedJust remember:
undefinedundefinedif (~str.indexOf(...)) reads as
"if found".undefinedundefined
To be
precise though, as big numbers are truncated to 32 bits by
undefinedundefined~ operator, there exist other
numbers that give undefinedundefined0, the smallest
is undefinedundefined~4294967295=0. That makes such
check correct only if a string is not that
long.undefinedundefined
Right now we can
see this trick only in the old code, as modern JavaScript
provides undefinedundefined.includes method (see
below).undefinedundefined
The more modern method
undefinedundefinedstr.includes(substr, pos)
returns undefinedundefinedtrue/false depending on
whether undefinedundefinedstr contains
undefinedundefinedsubstr within.undefinedundefined
It's the right choice if we need to test for the match, but don't need its position:
undefinedundefinedrun alert( "Widget with id".includes("Widget") ); // true
undefinedundefinedalert( "Hello".includes("Bye") ); // false
undefinedundefinedThe optional second argument of
undefinedundefinedstr.includes is the position to
start searching from:undefinedundefined
undefinedundefinedjs run alert( "Widget".includes("id") ); // true alert( "Widget".includes("id", 3) ); // false, from position 3 there is no "id"undefinedundefined
The methods undefinedundefinedstr.startsWith and undefinedundefinedstr.endsWith do exactly what they say:undefinedundefined
undefinedundefined
undefinedundefinedjs run alert( "Widget".startsWith("Wid") ); // true, "Widget" starts with "Wid" alert( "Widget".endsWith("get") ); // true, "Widget" ends with "get"undefinedundefined
There are 3 methods in
JavaScript to get a substring:
undefinedundefinedsubstring,
undefinedundefinedsubstr and
undefinedundefinedslice.undefinedundefined
str.slice(start [, end])undefinedundefined
Returns the part
of the string from undefinedundefinedstart to
(but not including)
undefinedundefinedend.undefinedundefined
For instance:
undefinedundefined
undefinedundefinedjs run let str = "stringify"; alert( str.slice(0, 5) ); // 'strin', the substring from 0 to 5 (not including 5) alert( str.slice(0, 1) ); // 's', from 0 to 1, but not including 1, so only character at 0undefinedundefined
If there is no second argument, then
undefinedundefinedslice goes till the end of
the string:undefinedundefined
undefinedundefinedjs run let str = "st*!*ringify*/!*"; alert( str.slice(2) ); // 'ringify', from the 2nd position till the endundefinedundefined
Negative values for
undefinedundefinedstart/end are also possible.
They mean the position is counted from the string
end:undefinedundefined
run let str = "strinundefinedundefined!gifundefinedundefined/!y";undefinedundefined
undefinedundefined// start at the 4th position from the right, end at the 1st from the right alert( str.slice(-4, -1) ); // ‘gif'
undefinedundefinedstr.substring(start [, end])undefinedundefined
Returns the part
of the string
undefinedundefinedbetweenundefinedundefinedstart
and undefinedundefinedend.undefinedundefined
This is almost the same as
undefinedundefinedslice, but it allows
undefinedundefinedstart to be greater than
undefinedundefinedend.undefinedundefined
For instance:
undefinedundefinedrun let str = "stundefinedundefined!ringundefinedundefined/!ify";undefinedundefined
undefinedundefined// these are same for substring alert( str.substring(2, 6) ); // "ring" alert( str.substring(6, 2) ); // "ring"
undefinedundefined// …but not for slice: alert( str.slice(2, 6) ); // "ring" (the same) alert( str.slice(6, 2) ); // "" (an empty string)
undefinedundefinedundefinedundefined
Negative arguments are (unlike slice)
not supported, they are treated as
undefinedundefined0.undefinedundefined
str.substr(start [, length])undefinedundefined
Returns the part
of the string from undefinedundefinedstart,
with the given
undefinedundefinedlength.undefinedundefined
In contrast with the previous methods,
this one allows us to specify the
undefinedundefinedlength instead of the ending
position:undefinedundefined
undefinedundefinedjs run let str = "st*!*ring*/!*ify"; alert( str.substr(2, 4) ); // 'ring', from the 2nd position get 4 charactersundefinedundefined
The first argument may be negative, to count from the end:
undefinedundefined
undefinedundefinedjs run let str = "strin*!*gi*/!*fy"; alert( str.substr(-4, 2) ); // 'gi', from the 4th position get 2 charactersundefinedundefined
Let's recap these methods to avoid any confusion:
undefinedundefined| method | undefinedundefinedselects… | undefinedundefinednegatives | undefinedundefined
|---|---|---|
undefinedundefinedslice(start, end)undefinedundefined
| undefinedundefinedfrom
undefinedundefinedstart to
undefinedundefinedend (not including
undefinedundefinedend)undefinedundefined |
undefinedundefinedallows negatives | undefinedundefined
undefinedundefinedsubstring(start, end)undefinedundefined
| undefinedundefinedbetween
undefinedundefinedstart and
undefinedundefinedendundefinedundefined |
undefinedundefinednegative values mean
undefinedundefined0undefinedundefined |
undefinedundefined
undefinedundefinedsubstr(start, length)undefinedundefined
| undefinedundefinedfrom
undefinedundefinedstart get
undefinedundefinedlength
charactersundefinedundefined | undefinedundefined
allows negative
undefinedundefinedstartundefinedundefined
| undefinedundefined
``undefinedundefinedsmart header="Which one to choose?" All of them can do the job. Formally,substr`
has a minor drawback: it is described not in the core JavaScript
specification, but in Annex B, which covers browser-only
features that exist mainly for historical reasons. So,
non-browser environments may fail to support it. But in practice
it works everywhere.undefinedundefined
Of the other two variants, undefinedundefinedslice
is a little bit more flexible, it allows negative arguments and
shorter to write. So, it's enough to remember solely
undefinedundefinedslice of these three methods.
undefinedundefined
As we know from the chapter undefinedundefinedinfo:comparison, strings are compared character-by-character in alphabetical order.undefinedundefined
undefinedundefinedAlthough, there are some oddities.
undefinedundefinedA lowercase letter is always greater than the uppercase:
undefinedundefined
undefinedundefinedjs run alert( 'a' > 'Z' ); // trueundefinedundefined
Letters with diacritical marks are "out of order":
undefinedundefined
undefinedundefinedjs run alert( 'Österreich' > 'Zealand' ); // trueundefinedundefined
This may lead to strange results if
we sort these country names. Usually people would expect
undefinedundefinedZealand to come after
undefinedundefinedÖsterreich in the
list.undefinedundefined
To understand what happens, let's review the internal representation of strings in JavaScript.
undefinedundefinedAll strings are encoded using undefinedundefinedUTF-16. That is: each character has a corresponding numeric code. There are special methods that allow to get the character for the code and back.undefinedundefined
undefinedundefinedstr.codePointAt(pos)undefinedundefined
Returns the code
for the character at position
undefinedundefinedpos:undefinedundefined
undefinedundefinedjs run // different case letters have different codes alert( "z".codePointAt(0) ); // 122 alert( "Z".codePointAt(0) ); // 90undefinedundefined
String.fromCodePoint(code)undefinedundefined
Creates a
character by its numeric
undefinedundefinedcodeundefinedundefined
undefinedundefinedjs run alert( String.fromCodePoint(90) ); // Zundefinedundefined
We can also add Unicode characters by
their codes using undefinedundefined\u followed
by the hex code:undefinedundefined
undefinedundefinedjs run // 90 is 5a in hexadecimal system alert( '\u005a' ); // Zundefinedundefined
Now let's see the characters with codes
undefinedundefined65..220 (the latin alphabet and a
little bit extra) by making a string of them:undefinedundefined
run let str = '';
undefinedundefinedfor (let i = 65; i <= 220; i++) { str += String.fromCodePoint(i); } alert( str ); // ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~ // ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜ
undefinedundefinedSee? Capital characters go first, then a
few special ones, then lowercase characters, and
undefinedundefinedÖ near the end of the
output.undefinedundefined
Now it becomes
obvious why
undefinedundefineda > Z.undefinedundefined
The characters are compared by their numeric
code. The greater code means that the character is greater. The
code for undefinedundefineda (97) is greater than
the code for undefinedundefinedZ
(90).undefinedundefined
Ö stand apart from the main
alphabet. Here, it's code is greater than anything from
undefinedundefineda to
undefinedundefinedz.undefinedundefinedThe "right" algorithm to do string comparisons is more complex than it may seem, because alphabets are different for different languages.
undefinedundefinedSo, the browser needs to know the language to compare.
undefinedundefinedLuckily, all modern browsers (IE10- requires the additional library undefinedundefinedIntl.js) support the internationalization standard undefinedundefinedECMA-402.undefinedundefined
undefinedundefinedIt provides a special method to compare strings in different languages, following their rules.
undefinedundefinedThe call undefinedundefinedstr.localeCompare(str2)
returns an integer indicating whether
undefinedundefinedstr is less, equal or greater
than undefinedundefinedstr2 according to the
language rules:undefinedundefined
str is less than
undefinedundefinedstr2.undefinedundefinedstr is greater than
undefinedundefinedstr2.undefinedundefined0
if they are equivalent.undefinedundefinedFor instance:
undefinedundefined
undefinedundefinedjs run alert( 'Österreich'.localeCompare('Zealand') ); // -1undefinedundefined
This method actually has two additional
arguments specified in undefinedundefinedthe documentation,
which allows it to specify the language (by default taken from
the environment, letter order depends on the language) and setup
additional rules like case sensitivity or should
undefinedundefined"a" and
undefinedundefined"á" be treated as the same
etc.undefinedundefined
warn header="Advanced knowledge" The section goes deeper into string internals. This knowledge will be useful for you if you plan to deal with emoji, rare mathematical or hieroglyphic characters or other rare symbols.
undefinedundefinedYou can skip the section if you don't plan to support them.
undefinedundefinedAll frequently used characters have 2-byte codes. Letters in most european languages, numbers, and even most hieroglyphs, have a 2-byte representation.
undefinedundefinedBut 2 bytes only allow 65536 combinations and that's not enough for every possible symbol. So rare symbols are encoded with a pair of 2-byte characters called "a surrogate pair".
undefinedundefinedThe length of such symbols is
undefinedundefined2:undefinedundefined
undefinedundefinedjs run alert( '𝒳'.length ); // 2, MATHEMATICAL SCRIPT CAPITAL X alert( '😂'.length ); // 2, FACE WITH TEARS OF JOY alert( '𩷶'.length ); // 2, a rare Chinese hieroglyphundefinedundefined
Note that surrogate pairs did not exist at the time when JavaScript was created, and thus are not correctly processed by the language!
undefinedundefinedWe
actually have a single symbol in each of the strings above, but
the undefinedundefinedlength shows a length of
undefinedundefined2.undefinedundefined
undefinedundefinedString.fromCodePoint and
undefinedundefinedstr.codePointAt are few rare
methods that deal with surrogate pairs right. They recently
appeared in the language. Before them, there were only
undefinedundefinedString.fromCharCode and
undefinedundefinedstr.charCodeAt. These
methods are actually the same as
undefinedundefinedfromCodePoint/codePointAt, but
don't work with surrogate pairs.undefinedundefined
Getting a symbol can be tricky, because surrogate pairs are treated as two characters:
undefinedundefined
undefinedundefinedjs run alert( '𝒳'[0] ); // strange symbols... alert( '𝒳'[1] ); // ...pieces of the surrogate pairundefinedundefined
Note that pieces of the surrogate pair have no meaning without each other. So the alerts in the example above actually display garbage.
undefinedundefined
Technically, surrogate pairs are also detectable by their codes:
if a character has the code in the interval of
undefinedundefined0xd800..0xdbff, then it is the
first part of the surrogate pair. The next character (second
part) must have the code in interval
undefinedundefined0xdc00..0xdfff. These intervals
are reserved exclusively for surrogate pairs by the
standard.undefinedundefined
In the case above:
undefinedundefinedrun // charCodeAt is not surrogate-pair aware, so it gives codes for parts
undefinedundefinedalert( ‘𝒳'.charCodeAt(0).toString(16) ); // d835, between 0xd800 and 0xdbff alert( ‘𝒳'.charCodeAt(1).toString(16) ); // dcb3, between 0xdc00 and 0xdfff
undefinedundefinedYou will find more ways to deal with surrogate pairs later in the chapter undefinedundefinedinfo:iterable. There are probably special libraries for that too, but nothing famous enough to suggest here.undefinedundefined
undefinedundefinedIn many languages there are symbols that are composed of the base character with a mark above/under it.
undefinedundefinedFor instance, the letter
undefinedundefineda can be the base character for:
undefinedundefinedàáâäãåā. Most common "composite"
character have their own code in the UTF-16 table. But not all
of them, because there are too many possible
combinations.undefinedundefined
To support arbitrary compositions, UTF-16 allows us to use several Unicode characters: the base character followed by one or many "mark" characters that "decorate" it.
undefinedundefined
For instance, if we have undefinedundefinedS
followed by the special "dot above" character (code
undefinedundefined\u0307), it is shown as
Ṡ.undefinedundefined
undefinedundefinedjs run alert( 'S\u0307' ); // Ṡundefinedundefined
If we need an additional mark above the letter (or below it) - no problem, just add the necessary mark character.
undefinedundefinedFor instance, if we append a
character "dot below" (code
undefinedundefined\u0323), then we'll have "S with
dots above and below":
undefinedundefinedṨ.undefinedundefined
For example:
undefinedundefined
undefinedundefinedjs run alert( 'S\u0307\u0323' ); // Ṩundefinedundefined
This provides great flexibility, but also an interesting problem: two characters may visually look the same, but be represented with different Unicode compositions.
undefinedundefinedFor instance:
run let s1 = 'S undefinedundefinedCode is prone to errors. You will quite likely make errors… Oh, what am I talking about? You are undefinedundefinedabsolutely going to make errors, at least if you're a human, not a undefinedundefinedrobot.undefinedundefined
undefinedundefinedBut in the browser, users don't see errors by default. So, if something goes wrong in the script, we won't see what's broken and can't fix it.
undefinedundefinedTo see errors and get a lot of other useful information about scripts, "developer tools" have been embedded in browsers.
undefinedundefinedMost developers lean towards Chrome or Firefox for development because those browsers have the best developer tools. Other browsers also provide developer tools, sometimes with special features, but are usually playing "catch-up" to Chrome or Firefox. So most developers have a "favorite" browser and switch to others if a problem is browser-specific.
undefinedundefinedDeveloper tools are potent; they have many features. To start, we'll learn how to open them, look at errors, and run JavaScript commands.
undefinedundefinedOpen the page undefinedundefinedbug.html.undefinedundefined
undefinedundefinedThere's an error in the JavaScript code on it. It's hidden from a regular visitor's eyes, so let's open developer tools to see it.
undefinedundefinedPress
undefinedundefinedkey:F12 or, if you're on Mac,
then
undefinedundefinedkey:Cmd+Opt+J.undefinedundefined
The developer tools will open on the Console tab by default.
undefinedundefinedIt looks somewhat like this:
undefinedundefined
undefinedundefinedThe exact look of developer tools depends on your version of Chrome. It changes from time to time but should be similar.
undefinedundefinedbug.html:12 with the line
number where the error has occurred.undefinedundefinedBelow the error
message, there is a blue undefinedundefined>
symbol. It marks a "command line" where we can type JavaScript
commands. Press undefinedundefinedkey:Enter to run
them.undefinedundefined
Now we can see errors, and that's enough for a start. We'll come back to developer tools later and cover debugging more in-depth in the chapter undefinedundefinedinfo:debugging-chrome.undefinedundefined
undefinedundefined
``undefinedundefinedsmart header="Multi-line input" Usually, when we put a line of code into the console, and then presskey:Enter`,
it executes.undefinedundefined
To insert
multiple lines, press
undefinedundefinedkey:Shift+Enter. This way one can
enter long fragments of JavaScript code.
undefinedundefined
Most other browsers use
undefinedundefinedkey:F12 to open developer
tools.undefinedundefined
The look & feel of them is quite similar. Once you know how to use one of these tools (you can start with Chrome), you can easily switch to another.
undefinedundefinedSafari (Mac browser, not supported by Windows/Linux) is a little bit special here. We need to enable the "Develop menu" first.
undefinedundefinedOpen Preferences and go to the "Advanced" pane. There's a checkbox at the bottom:
undefinedundefined
undefinedundefinedNow
undefinedundefinedkey:Cmd+Opt+C can toggle the
console. Also, note that the new top menu item named "Develop"
has appeared. It has many commands and
options.undefinedundefined
key:F12 for most browsers on
Windows. Chrome for Mac needs
undefinedundefinedkey:Cmd+Opt+J, Safari:
undefinedundefinedkey:Cmd+Opt+C (need to enable
first).undefinedundefinedNow we have the environment ready. In the next section, we'll get down to JavaScript.
undefinedundefinedObjects allow you to store keyed collections of values. That's fine.
undefinedundefinedBut quite often we find that we need an undefinedundefinedordered collection, where we have a 1st, a 2nd, a 3rd element and so on. For example, we need that to store a list of something: users, goods, HTML elements etc.undefinedundefined
undefinedundefinedIt is not convenient to use an object here, because it provides no methods to manage the order of elements. We can't insert a new property "between" the existing ones. Objects are just not meant for such use.
undefinedundefinedThere exists a special data
structure named undefinedundefinedArray, to store
ordered collections.undefinedundefined
There are two syntaxes for creating an empty array:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet arr undefinedundefined=undefinedundefinednewundefinedundefinedArray()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedlet arr undefinedundefined= []undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Almost all the time, the second syntax is used. We can supply initial elements in the brackets:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet fruits undefinedundefined= [undefinedundefined"Apple"undefinedundefined,undefinedundefined"Orange"undefinedundefined,undefinedundefined"Plum"]undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Array elements are numbered, starting with zero.
undefinedundefinedWe can get an element by its number in square brackets:
undefinedundefinedrun let fruits = ["Apple", "Orange", "Plum"];
undefinedundefinedalert( fruits[0] ); // Apple alert( fruits[1] ); // Orange alert( fruits[2] ); // Plum
undefinedundefinedWe can replace an element:
undefinedundefinedundefinedundefinedundefinedundefinedfruits[undefinedundefined2] undefinedundefined=undefinedundefined'Pear'undefinedundefined;undefinedundefined// now ["Apple", "Orange", "Pear"]undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…Or add a new one to the array:
undefinedundefinedundefinedundefinedundefinedundefinedfruits[undefinedundefined3] undefinedundefined=undefinedundefined'Lemon'undefinedundefined;undefinedundefined// now ["Apple", "Orange", "Pear", "Lemon"]undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The total count of the elements in the
array is its
undefinedundefinedlength:undefinedundefined
run let fruits = ["Apple", "Orange", "Plum"];
undefinedundefinedalert( fruits.length ); // 3
undefinedundefinedWe can also use
undefinedundefinedalert to show the whole
array.undefinedundefined
run let fruits = ["Apple", "Orange", "Plum"];
undefinedundefinedalert( fruits ); // Apple,Orange,Plum
undefinedundefinedAn array can store elements of any type.
undefinedundefinedFor instance:
undefinedundefinedrun no-beautify // mix of values let arr = [ ‘Apple', { name: ‘John' }, true, function() { alert(‘hello'); } ];
undefinedundefined// get the object at index 1 and then show its name alert( arr[1].name ); // John
undefinedundefined// get the function at index 3 and run it arrundefinedundefined3; // hello undefinedundefined
undefinedundefined
`undefinedundefinedsmart header="Trailing comma" An array, just like an object, may end with a comma:js
let fruits = [ "Apple", "Orange",
"Plum"undefinedundefined!,undefinedundefined/!];undefinedundefined
undefinedundefined
The "trailing comma" style makes it easier to insert/remove items, because all lines become alike.undefinedundefined
undefinedundefinedA undefinedundefinedqueue is one of the most common uses of an array. In computer science, this means an ordered collection of elements which supports two operations:undefinedundefined
undefinedundefinedpush
appends an element to the end.undefinedundefinedshift get
an element from the beginning, advancing the queue, so that
the 2nd element becomes the 1st.undefinedundefined
undefinedundefinedundefinedundefined
Arrays support both operations.
undefinedundefinedIn practice we need it very often. For example, a queue of messages that need to be shown on-screen.
undefinedundefinedThere's another use case for arrays - the data structure named undefinedundefinedstack.undefinedundefined
undefinedundefinedIt supports two operations:
undefinedundefinedpush adds an element to the
end.undefinedundefinedpop takes an element from the
end.undefinedundefinedSo new elements are added or taken always from the "end".
undefinedundefinedA stack is usually illustrated as a pack of cards: new cards are added to the top or taken from the top:
undefinedundefined
undefinedundefinedundefinedundefined
For stacks, the latest pushed item is received first, that's also called LIFO (Last-In-First-Out) principle. For queues, we have FIFO (First-In-First-Out).
undefinedundefinedArrays in JavaScript can work both as a queue and as a stack. They allow you to add/remove elements both to/from the beginning or the end.
undefinedundefinedIn computer science the data structure that allows this, is called undefinedundefineddeque.undefinedundefined
undefinedundefinedundefinedundefinedMethods that work with the end of the array:undefinedundefined
undefinedundefinedpopundefinedundefinedExtracts the last element of the array and returns it:
undefinedundefinedrun let fruits = ["Apple", "Orange", "Pear"];
undefinedundefinedalert( fruits.pop() ); // remove "Pear" and alert it
undefinedundefinedalert( fruits ); // Apple, Orange
undefinedundefinedpushundefinedundefinedAppend the element to the end of the array:
undefinedundefinedrun let fruits = ["Apple", "Orange"];
undefinedundefinedfruits.push("Pear");
undefinedundefinedalert( fruits ); // Apple, Orange, Pear
undefinedundefinedThe call
undefinedundefinedfruits.push(...) is equal to
undefinedundefinedfruits[fruits.length] = ....undefinedundefined
undefinedundefinedMethods that work with the beginning of the array:undefinedundefined
undefinedundefinedshiftundefinedundefinedExtracts the first element of the array and returns it:
undefinedundefinedrun let fruits = ["Apple", "Orange", "Pear"];
undefinedundefinedalert( fruits.shift() ); // remove Apple and alert it
undefinedundefinedalert( fruits ); // Orange, Pear
undefinedundefinedunshiftundefinedundefinedAdd the element to the beginning of the array:
undefinedundefinedrun let fruits = ["Orange", "Pear"];
undefinedundefinedfruits.unshift(‘Apple');
undefinedundefinedalert( fruits ); // Apple, Orange, Pear
undefinedundefinedMethods undefinedundefinedpush
and undefinedundefinedunshift can add multiple
elements at once:undefinedundefined
run let fruits = ["Apple"];
undefinedundefinedfruits.push("Orange", "Peach"); fruits.unshift("Pineapple", "Lemon");
undefinedundefined// ["Pineapple", "Lemon", "Apple", "Orange", "Peach"] alert( fruits );
undefinedundefinedAn array is a special kind of object. The
square brackets used to access a property
undefinedundefinedarr[0] actually come from the
object syntax. That's essentially the same as
undefinedundefinedobj[key], where
undefinedundefinedarr is the object, while numbers
are used as keys.undefinedundefined
They
extend objects providing special methods to work with ordered
collections of data and also the
undefinedundefinedlength property. But at the core
it's still an object.undefinedundefined
Remember, there are only eight basic data types in JavaScript (see the undefinedundefinedData types chapter for more info). Array is an object and thus behaves like an object.undefinedundefined
undefinedundefinedFor instance, it is copied by reference:
undefinedundefinedrun let fruits = ["Banana"]
undefinedundefinedlet arr = fruits; // copy by reference (two variables reference the same array)
undefinedundefinedalert( arr === fruits ); // true
undefinedundefinedarr.push("Pear"); // modify the array by reference
undefinedundefinedalert( fruits ); // Banana, Pear - 2 items now
undefinedundefined…But what makes arrays really special is their internal representation. The engine tries to store its elements in the contiguous memory area, one after another, just as depicted on the illustrations in this chapter, and there are other optimizations as well, to make arrays work really fast.
undefinedundefinedBut they all break if we quit working with an array as with an "ordered collection" and start working with it as if it were a regular object.
undefinedundefinedFor instance, technically we can do this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet fruits undefinedundefined= []undefinedundefined;undefinedundefined// make an arrayundefinedundefinedundefinedundefinedundefinedundefinedfruits[undefinedundefined99999] undefinedundefined=undefinedundefined5undefinedundefined;undefinedundefined// assign a property with the index far greater than its lengthundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfruits.undefinedundefinedageundefinedundefined=undefinedundefined25undefinedundefined;undefinedundefined// create a property with an arbitrary nameundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That's possible, because arrays are objects at their base. We can add any properties to them.
undefinedundefinedBut the engine will see that we're working with the array as with a regular object. Array-specific optimizations are not suited for such cases and will be turned off, their benefits disappear.
undefinedundefinedThe ways to misuse an array:
undefinedundefinedarr.test = 5.undefinedundefined
arr[0] and then
undefinedundefinedarr[1000] (and nothing between
them).undefinedundefinedarr[1000],
undefinedundefinedarr[999] and so
on.undefinedundefinedPlease think of arrays as special structures
to work with the undefinedundefinedordered data. They
provide special methods for that. Arrays are carefully tuned
inside JavaScript engines to work with contiguous ordered data,
please use them this way. And if you need arbitrary keys,
chances are high that you actually require a regular object
undefinedundefined{}.undefinedundefined
Methods
undefinedundefinedpush/pop run fast, while
undefinedundefinedshift/unshift are
slow.undefinedundefined
undefinedundefinedundefinedundefined
Why is it faster to work with the end of an array than with its beginning? Let's see what happens during the execution:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfruits.undefinedundefinedshift()undefinedundefined;undefinedundefined// take 1 element from the startundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
It's not enough to take and remove the
element with the number undefinedundefined0. Other
elements need to be renumbered as well.undefinedundefined
The undefinedundefinedshift
operation must do 3 things:undefinedundefined
0.undefinedundefined1 to
undefinedundefined0, from
undefinedundefined2 to
undefinedundefined1 and so on.undefinedundefined
length
property.undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefinedThe more elements in the array, the more time to move them, more in-memory operations.undefinedundefined
undefinedundefinedThe similar thing happens with
undefinedundefinedunshift: to add an element to the
beginning of the array, we need first to move existing elements
to the right, increasing their indexes.undefinedundefined
And what's with
undefinedundefinedpush/pop? They do not need to
move anything. To extract an element from the end, the
undefinedundefinedpop method cleans the index and
shortens
undefinedundefinedlength.undefinedundefined
The actions for the
undefinedundefinedpop operation:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfruits.undefinedundefinedpop()undefinedundefined;undefinedundefined// take 1 element from the endundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedundefinedundefined
undefinedundefinedThe
undefinedundefinedpop method does not need to
move anything, because other elements keep their indexes.
That's why it's blazingly
fast.undefinedundefinedundefinedundefined
The similar thing with the
undefinedundefinedpush method.undefinedundefined
One of the oldest ways to cycle array items is the
undefinedundefinedfor loop over
indexes:undefinedundefined
run let arr = ["Apple", "Orange", "Pear"];
undefinedundefinedundefinedundefined! for (let i = 0; i < arr.length; i++) { undefinedundefined/! alert( arr[i] ); } undefinedundefined
undefinedundefinedBut for arrays there
is another form of loop,
undefinedundefinedfor..of:undefinedundefined
run let fruits = ["Apple", "Orange", "Plum"];
undefinedundefined// iterates over array elements for (let fruit of fruits) { alert( fruit ); }
undefinedundefinedThe
undefinedundefinedfor..of doesn't give access to
the number of the current element, just its value, but in most
cases that's enough. And it's shorter.undefinedundefined
Technically, because arrays are objects, it
is also possible to use
undefinedundefinedfor..in:undefinedundefined
run let arr = ["Apple", "Orange", "Pear"];
undefinedundefinedundefinedundefined! for (let key in arr) { undefinedundefined/! alert( arr[key] ); // Apple, Orange, Pear } undefinedundefined
undefinedundefinedBut that's actually a bad idea. There are potential problems with it:
undefinedundefinedThe loop
undefinedundefinedfor..in iterates over
undefinedundefinedall properties, not only the
numeric ones.undefinedundefined
There are so-called "array-like" objects in the browser and
in other environments, that undefinedundefinedlook like
arrays. That is, they have
undefinedundefinedlength and indexes
properties, but they may also have other non-numeric
properties and methods, which we usually don't need. The
undefinedundefinedfor..in loop will list them
though. So if we need to work with array-like objects, then
these "extra" properties can become a
problem.undefinedundefined
The
undefinedundefinedfor..in loop is optimized for
generic objects, not arrays, and thus is 10-100 times
slower. Of course, it's still very fast. The speedup may
only matter in bottlenecks. But still we should be aware of
the difference.undefinedundefined
Generally, we
shouldn't use undefinedundefinedfor..in for
arrays.undefinedundefined
The undefinedundefinedlength
property automatically updates when we modify the array. To be
precise, it is actually not the count of values in the array,
but the greatest numeric index plus one.undefinedundefined
For instance, a single element with a large index gives a big length:
undefinedundefinedrun let fruits = []; fruits[123] = "Apple";
undefinedundefinedalert( fruits.length ); // 124
undefinedundefinedNote that we usually don't use arrays like that.
undefinedundefinedAnother interesting thing
about the undefinedundefinedlength property is that
it's writable.undefinedundefined
If we increase it manually, nothing interesting happens. But if we decrease it, the array is truncated. The process is irreversible, here's the example:
undefinedundefinedrun let arr = [1, 2, 3, 4, 5];
undefinedundefinedarr.length = 2; // truncate to 2 elements alert( arr ); // [1, 2]
undefinedundefinedarr.length = 5; // return length back alert( arr[3] ); // undefined: the values do not return
undefinedundefinedSo, the simplest way to clear the array
is:
undefinedundefinedarr.length = 0;.undefinedundefined
There is one more syntax to create an array:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet arr undefinedundefined=undefinedundefined*!*undefinedundefinednew Arrayundefinedundefined*undefinedundefined/!undefinedundefined*(undefinedundefined"Apple", "Pear", "etc"undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
It's rarely used, because square
brackets undefinedundefined[] are shorter. Also
there's a tricky feature with it.undefinedundefined
If undefinedundefinednew Array
is called with a single argument which is a number, then it
creates an array undefinedundefinedwithout items, but with
the given length.undefinedundefined
Let's see how one can shoot themself in the foot:
undefinedundefinedrun let arr = new Array(2); // will it create an array of [2] ?
undefinedundefinedalert( arr[0] ); // undefined! no elements.
undefinedundefinedalert( arr.length ); // length 2
undefinedundefinedIn the code above,
undefinedundefinednew Array(number) has all
elements
undefinedundefinedundefined.undefinedundefined
To evade such surprises, we usually use square brackets, unless we really know what we're doing.
undefinedundefinedArrays can have items that are also arrays. We can use it for multidimensional arrays, for example to store matrices:
undefinedundefinedrun let matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9]];
undefinedundefinedalert( matrix[1][1] ); // 5, the central element
undefinedundefinedArrays have their own implementation of
undefinedundefinedtoString method that returns a
comma-separated list of elements.undefinedundefined
For instance:
undefinedundefinedrun let arr = [1, 2, 3];
undefinedundefinedalert( arr ); // 1,2,3 alert( String(arr) === ‘1,2,3' ); // true
undefinedundefinedAlso, let's try this:
undefinedundefined
undefinedundefinedjs run alert( [] + 1 ); // "1" alert( [1] + 1 ); // "11" alert( [1,2] + 1 ); // "1,21"undefinedundefined
Arrays do not have
undefinedundefinedSymbol.toPrimitive, neither a
viable undefinedundefinedvalueOf, they implement
only undefinedundefinedtoString conversion, so here
undefinedundefined[] becomes an empty string,
undefinedundefined[1] becomes
undefinedundefined"1" and
undefinedundefined[1,2] becomes
undefinedundefined"1,2".undefinedundefined
When the binary plus
undefinedundefined"+" operator adds something to a
string, it converts it to a string as well, so the next step
looks like this:undefinedundefined
undefinedundefinedjs run alert( "" + 1 ); // "1" alert( "1" + 1 ); // "11" alert( "1,2" + 1 ); // "1,21"undefinedundefined
Arrays in
JavaScript, unlike some other programming languages, shouldn't
be compared with operator
undefinedundefined==.undefinedundefined
This operator has no special treatment for arrays, it works with them as with any objects.
undefinedundefinedLet's recall the rules:
undefinedundefined== only if they're references
to the same object.undefinedundefined==
is an object, and the other one is a primitive, then the
object gets converted to primitive, as explained in the
chapter undefinedundefinedinfo:object-toprimitive.undefinedundefined
null and
undefinedundefinedundefined that equal
undefinedundefined== each other and nothing
else.undefinedundefinedThe strict comparison
undefinedundefined=== is even simpler, as it
doesn't convert types.undefinedundefined
So, if we compare arrays with
undefinedundefined==, they are never the same,
unless we compare two variables that reference exactly the same
array.undefinedundefined
For example:
undefinedundefinedjs run alert( [] == [] ); // false alert( [0] == [0] ); // falseundefinedundefined
These arrays are technically different
objects. So they aren't equal. The
undefinedundefined== operator doesn't do
item-by-item comparison.undefinedundefined
Comparison with primitives may give seemingly strange results as well:
undefinedundefinedrun alert( 0 == [] ); // true
undefinedundefinedalert(‘0' == [] ); // false
undefinedundefinedHere, in both cases, we compare a
primitive with an array object. So the array
undefinedundefined[] gets converted to primitive
for the purpose of comparison and becomes an empty string
undefinedundefined''.undefinedundefined
Then the comparison process goes on with the primitives, as described in the chapter undefinedundefinedinfo:type-conversions:undefinedundefined
undefinedundefinedrun // after [] was converted to '' alert( 0 == '' ); // true, as '' becomes converted to number 0
undefinedundefinedalert(‘0' == '' ); // false, no type conversion, different strings
undefinedundefinedSo, how to compare arrays?
undefinedundefinedThat's simple: don't use the
undefinedundefined== operator. Instead, compare
them item-by-item in a loop or using iteration methods explained
in the next chapter.undefinedundefined
Array is a special kind of object, suited to storing and managing ordered data items.
undefinedundefinedThe declaration:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// square brackets (usual)undefinedundefinedundefinedundefinedundefinedundefinedlet arr undefinedundefined= [item1undefinedundefined,undefinedundefineditem2...]undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// new Array (exceptionally rare)undefinedundefinedundefinedundefinedundefinedundefinedlet arr undefinedundefined=undefinedundefinednewundefinedundefinedArray(item1undefinedundefined,undefinedundefineditem2...)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The call to
undefinedundefinednew Array(number) creates an
array with the given length, but without
elements.undefinedundefined
length
property is the array length or, to be precise, its last
numeric index plus one. It is auto-adjusted by array
methods.undefinedundefinedIf we shorten
undefinedundefinedlength manually, the array is
truncated.undefinedundefined
We can use an array as a deque with the following operations:
undefinedundefinedpush(...items) adds
undefinedundefineditems to the
end.undefinedundefinedpop() removes the element from
the end and returns it.undefinedundefinedshift()
removes the element from the beginning and returns
it.undefinedundefinedunshift(...items) adds
undefinedundefineditems to the
beginning.undefinedundefinedTo loop over the elements of the array: -
undefinedundefinedfor (let i=0; i<arr.length; i++)
- works fastest, old-browser-compatible. -
undefinedundefinedfor (let item of arr) - the
modern syntax for items only, -
undefinedundefinedfor (let i in arr) - never
use.undefinedundefined
To compare
arrays, don't use the undefinedundefined== operator
(as well as undefinedundefined>,
undefinedundefined< and others), as they have no
special treatment for arrays. They handle them as any objects,
and it's not what we usually want.undefinedundefined
Instead you can use
undefinedundefinedfor..of loop to compare arrays
item-by-item.undefinedundefined
We will continue with arrays and study more methods to add, remove, extract elements and sort arrays in the next chapter undefinedundefinedinfo:array-methods.undefinedundefined
undefinedundefinedArrays provide a lot of methods. To make things easier, in this chapter they are split into groups.
undefinedundefinedWe already know methods that add and remove items from the beginning or the end:
undefinedundefinedarr.push(...items) - adds items
to the end,undefinedundefinedarr.pop() - extracts an item
from the end,undefinedundefinedarr.shift() - extracts an item
from the beginning,undefinedundefinedarr.unshift(...items) - adds
items to the beginning.undefinedundefinedHere are a few others.
undefinedundefinedHow to delete an element from the array?
undefinedundefinedThe arrays are objects, so we can try to use
undefinedundefineddelete:undefinedundefined
run let arr = ["I", "go", "home"];
undefinedundefineddelete arr[1]; // remove "go"
undefinedundefinedalert( arr[1] ); // undefined
undefinedundefined// now arr = ["I", , "home"]; alert( arr.length ); // 3
undefinedundefinedThe element was removed, but the array
still has 3 elements, we can see that
undefinedundefinedarr.length == 3.undefinedundefined
That's natural, because
undefinedundefineddelete obj.key removes a value by
the undefinedundefinedkey. It's all it does. Fine
for objects. But for arrays we usually want the rest of elements
to shift and occupy the freed place. We expect to have a shorter
array now.undefinedundefined
So, special methods should be used.
undefinedundefinedThe undefinedundefinedarr.splice method is a swiss army knife for arrays. It can do everything: insert, remove and replace elements.undefinedundefined
undefinedundefinedThe syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedarr.undefinedundefinedsplice(start[undefinedundefined, deleteCountundefinedundefined, elem1undefinedundefined, ...undefinedundefined, elemN])undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
It modifies
undefinedundefinedarr starting from the index
undefinedundefinedstart: removes
undefinedundefineddeleteCount elements and then
inserts undefinedundefinedelem1, ..., elemN at
their place. Returns the array of removed
elements.undefinedundefined
This method is easy to grasp by examples.
undefinedundefinedLet's start with the deletion:
undefinedundefinedrun let arr = ["I", "study", "JavaScript"];
undefinedundefinedundefinedundefined! arr.splice(1, 1); // from index 1 remove 1 element undefinedundefined/!undefinedundefined
undefinedundefinedalert( arr ); // ["I", "JavaScript"]
undefinedundefinedEasy, right? Starting from the index
undefinedundefined1 it removed
undefinedundefined1 element.undefinedundefined
In the next example we remove 3 elements and replace them with the other two:
undefinedundefinedrun let arr = [undefinedundefined!"I", "study", "JavaScript",undefinedundefined/! "right", "now"];undefinedundefined
undefinedundefined// remove 3 first elements and replace them with another arr.splice(0, 3, "Let's", "dance");
undefinedundefinedalert( arr ) // now [undefinedundefined!"Let's", "dance"undefinedundefined/!, "right", "now"] undefinedundefined
undefinedundefinedHere we can see that
undefinedundefinedsplice returns the array of
removed elements:undefinedundefined
run let arr = [undefinedundefined!"I", "study",undefinedundefined/! "JavaScript", "right", "now"];undefinedundefined
undefinedundefined// remove 2 first elements let removed = arr.splice(0, 2);
undefinedundefinedalert( removed ); // "I", "study" <- array of removed elements
undefinedundefinedThe undefinedundefinedsplice
method is also able to insert the elements without any removals.
For that we need to set
undefinedundefineddeleteCount to
undefinedundefined0:undefinedundefined
run let arr = ["I", "study", "JavaScript"];
undefinedundefined// from index 2 // delete 0 // then insert "complex" and "language" arr.splice(2, 0, "complex", "language");
undefinedundefinedalert( arr ); // "I", "study", "complex", "language", "JavaScript"
undefinedundefinedsmart header="Negative indexes allowed" Here and in other array methods, negative indexes are allowed. They specify the position from the end of the array, like here:
undefinedundefinedrun let arr = [1, 2, 5];
undefinedundefined// from index -1 (one step from the end) // delete 0 elements, // then insert 3 and 4 arr.splice(-1, 0, 3, 4);
undefinedundefinedalert( arr ); // 1,2,3,4,5
undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The method undefinedundefinedarr.slice is much simpler than
similar-looking
undefinedundefinedarr.splice.undefinedundefined
The syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedarr.undefinedundefinedslice([start]undefinedundefined, [end])undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
It returns a new array copying to it
all items from index undefinedundefinedstart to
undefinedundefinedend (not including
undefinedundefinedend). Both
undefinedundefinedstart and
undefinedundefinedend can be negative, in that case
position from array end is assumed.undefinedundefined
It's similar to a string method
undefinedundefinedstr.slice, but instead of
substrings it makes subarrays.undefinedundefined
For instance:
undefinedundefinedrun let arr = ["t", "e", "s", "t"];
undefinedundefinedalert( arr.slice(1, 3) ); // e,s (copy from 1 to 3)
undefinedundefinedalert( arr.slice(-2) ); // s,t (copy from -2 till the end)
undefinedundefinedWe can also call it without arguments:
undefinedundefinedarr.slice() creates a copy of
undefinedundefinedarr. That's often used to obtain
a copy for further transformations that should not affect the
original array.undefinedundefined
The method undefinedundefinedarr.concat creates a new array that includes values from other arrays and additional items.undefinedundefined
undefinedundefinedThe syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedarr.undefinedundefinedconcat(arg1undefinedundefined,undefinedundefinedarg2...)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
It accepts any number of arguments - either arrays or values.
undefinedundefinedThe result is a
new array containing items from
undefinedundefinedarr, then
undefinedundefinedarg1,
undefinedundefinedarg2 etc.undefinedundefined
If an argument
undefinedundefinedargN is an array, then all its
elements are copied. Otherwise, the argument itself is
copied.undefinedundefined
For instance:
undefinedundefinedrun let arr = [1, 2];
undefinedundefined// create an array from: arr and [3,4] alert( arr.concat([3, 4]) ); // 1,2,3,4
undefinedundefined// create an array from: arr and [3,4] and [5,6] alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6
undefinedundefined// create an array from: arr and [3,4], then add values 5 and 6 alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6
undefinedundefinedNormally, it only copies elements from arrays. Other objects, even if they look like arrays, are added as a whole:
undefinedundefinedrun let arr = [1, 2];
undefinedundefinedlet arrayLike = { 0: "something", length: 1 };
undefinedundefinedalert( arr.concat(arrayLike) ); // 1,2,[object Object]
undefinedundefined…But if an array-like object has a
special undefinedundefinedSymbol.isConcatSpreadable
property, then it's treated as an array by
undefinedundefinedconcat: its elements are added
instead:undefinedundefined
run let arr = [1, 2];
undefinedundefinedlet arrayLike = { 0: "something", 1: "else", undefinedundefined! [Symbol.isConcatSpreadable]: true, undefinedundefined/! length: 2 };undefinedundefined
undefinedundefinedalert( arr.concat(arrayLike) ); // 1,2,something,else
undefinedundefinedThe undefinedundefinedarr.forEach method allows to run a function for every element of the array.undefinedundefined
undefinedundefinedThe syntax:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedarr.undefinedundefinedforEach(undefinedundefinedfunction(itemundefinedundefined, indexundefinedundefined, array) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ... do something with itemundefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
For instance, this shows each element of the array:
undefinedundefined
undefinedundefinedjs run // for each element call alert ["Bilbo", "Gandalf", "Nazgul"].forEach(alert);undefinedundefined
And this code is more elaborate about their positions in the target array:
undefinedundefined
undefinedundefinedjs run ["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => { alert(`${item} is at index ${index} in ${array}`); });undefinedundefined
The result of the function (if it returns any) is thrown away and ignored.
undefinedundefinedNow let's cover methods that search in an array.
undefinedundefinedThe methods undefinedundefinedarr.indexOf, undefinedundefinedarr.lastIndexOf and undefinedundefinedarr.includes have the same syntax and do essentially the same as their string counterparts, but operate on items instead of characters:undefinedundefined
undefinedundefinedarr.indexOf(item, from) - looks
for undefinedundefineditem starting from index
undefinedundefinedfrom, and returns the index
where it was found, otherwise
undefinedundefined-1.undefinedundefinedarr.lastIndexOf(item, from) -
same, but looks for from right to left.undefinedundefinedarr.includes(item, from) -
looks for undefinedundefineditem starting from
index undefinedundefinedfrom, returns
undefinedundefinedtrue if
found.undefinedundefinedFor instance:
undefinedundefinedrun let arr = [1, 0, false];
undefinedundefinedalert( arr.indexOf(0) ); // 1 alert( arr.indexOf(false) ); // 2 alert( arr.indexOf(null) ); // -1
undefinedundefinedalert( arr.includes(1) ); // true
undefinedundefinedNote that the methods use
undefinedundefined=== comparison. So, if we look
for undefinedundefinedfalse, it finds exactly
undefinedundefinedfalse and not the
zero.undefinedundefined
If we want to
check for inclusion, and don't want to know the exact index,
then undefinedundefinedarr.includes is
preferred.undefinedundefined
Also, a
very minor difference of undefinedundefinedincludes
is that it correctly handles undefinedundefinedNaN,
unlike
undefinedundefinedindexOf/lastIndexOf:undefinedundefined
undefinedundefinedjs run const arr = [NaN]; alert( arr.indexOf(NaN) ); // -1 (should be 0, but === equality doesn't work for NaN) alert( arr.includes(NaN) );// true (correct)undefinedundefined
Imagine we have an array of objects. How do we find an object with the specific condition?
undefinedundefinedHere the undefinedundefinedarr.find(fn) method comes in handy.undefinedundefined
undefinedundefinedThe syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet result undefinedundefined=undefinedundefinedarr.undefinedundefinedfind(undefinedundefinedfunction(itemundefinedundefined, indexundefinedundefined, array) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// if true is returned, item is returned and iteration is stoppedundefinedundefinedundefinedundefinedundefinedundefined// for falsy scenario returns undefinedundefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The function is called for elements of the array, one after another:
undefinedundefineditem is the
element.undefinedundefinedindex is its
index.undefinedundefinedarray is the array
itself.undefinedundefinedIf it returns
undefinedundefinedtrue, the search is stopped, the
undefinedundefineditem is returned. If nothing
found, undefinedundefinedundefined is
returned.undefinedundefined
For example,
we have an array of users, each with the fields
undefinedundefinedid and
undefinedundefinedname. Let's find the one with
undefinedundefinedid == 1:undefinedundefined
run let users = [ {id: 1, name: "John"}, {id: 2, name: "Pete"}, {id: 3, name: "Mary"}];
undefinedundefinedlet user = users.find(item => item.id == 1);
undefinedundefinedalert(user.name); // John
undefinedundefinedIn real life arrays of objects is a
common thing, so the undefinedundefinedfind method
is very useful.undefinedundefined
Note
that in the example we provide to
undefinedundefinedfind the function
undefinedundefineditem => item.id == 1 with one
argument. That's typical, other arguments of this function are
rarely used.undefinedundefined
The
undefinedundefinedarr.findIndex method is
essentially the same, but it returns the index where the element
was found instead of the element itself and
undefinedundefined-1 is returned when nothing is
found.undefinedundefined
The
undefinedundefinedfind method looks for a single
(first) element that makes the function return
undefinedundefinedtrue.undefinedundefined
If there may be many, we can use undefinedundefinedarr.filter(fn).undefinedundefined
undefinedundefinedThe syntax is similar to
undefinedundefinedfind, but
undefinedundefinedfilter returns an array of all
matching elements:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet results undefinedundefined=undefinedundefinedarr.undefinedundefinedfilter(undefinedundefinedfunction(itemundefinedundefined, indexundefinedundefined, array) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// if true item is pushed to results and the iteration continuesundefinedundefinedundefinedundefinedundefinedundefined// returns empty array if nothing foundundefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
For instance:
undefinedundefinedrun let users = [ {id: 1, name: "John"}, {id: 2, name: "Pete"}, {id: 3, name: "Mary"}];
undefinedundefined// returns array of the first two users let someUsers = users.filter(item => item.id < 3);
undefinedundefinedalert(someUsers.length); // 2
undefinedundefinedLet's move on to methods that transform and reorder an array.
undefinedundefinedThe undefinedundefinedarr.map method is one of the most useful and often used.undefinedundefined
undefinedundefinedIt calls the function for each element of the array and returns the array of results.
undefinedundefinedThe syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet result undefinedundefined=undefinedundefinedarr.undefinedundefinedmap(undefinedundefinedfunction(itemundefinedundefined, indexundefinedundefined, array) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// returns the new value instead of itemundefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
For instance, here we transform each element into its length:
undefinedundefined
undefinedundefinedjs run let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length); alert(lengths); // 5,7,6undefinedundefined
The call to undefinedundefinedarr.sort() sorts the array undefinedundefinedin place, changing its element order.undefinedundefined
undefinedundefinedIt also returns
the sorted array, but the returned value is usually ignored, as
undefinedundefinedarr itself is
modified.undefinedundefined
For instance:
undefinedundefinedrun let arr = [ 1, 2, 15 ];
undefinedundefined// the method reorders the content of arr arr.sort();
undefinedundefinedalert( arr ); // undefinedundefined!1, 15, 2undefinedundefined/! undefinedundefined
undefinedundefinedDid you notice anything strange in the outcome?
undefinedundefinedThe
order became undefinedundefined1, 15, 2. Incorrect.
But why?undefinedundefined
undefinedundefinedThe items are sorted as strings by default.undefinedundefined
undefinedundefined
Literally, all elements are converted to strings for
comparisons. For strings, lexicographic ordering is applied and
indeed
undefinedundefined"2" > "15".undefinedundefined
To use our own sorting order, we need to
supply a function as the argument of
undefinedundefinedarr.sort().undefinedundefined
The function should compare two arbitrary values and return:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedcompare(aundefinedundefined, b) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (a undefinedundefined> b) undefinedundefinedreturnundefinedundefined1undefinedundefined;undefinedundefined// if the first value is greater than the secondundefinedundefinedundefinedundefinedundefinedundefinedif (a undefinedundefined== b) undefinedundefinedreturnundefinedundefined0undefinedundefined;undefinedundefined// if values are equalundefinedundefinedundefinedundefinedundefinedundefinedif (a undefinedundefined< b) undefinedundefinedreturnundefinedundefined-1undefinedundefined;undefinedundefined// if the first value is less than the secondundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
For instance, to sort as numbers:
undefinedundefinedrun function compareNumeric(a, b) { if (a > b) return 1; if (a == b) return 0; if (a < b) return -1; }
undefinedundefinedlet arr = [ 1, 2, 15 ];
undefinedundefinedundefinedundefined! arr.sort(compareNumeric); undefinedundefined/!undefinedundefined
undefinedundefinedalert(arr); // undefinedundefined!1, 2, 15undefinedundefined/! undefinedundefined
undefinedundefinedNow it works as intended.
undefinedundefinedLet's step aside and think
what's happening. The undefinedundefinedarr can be
array of anything, right? It may contain numbers or strings or
objects or whatever. We have a set of undefinedundefinedsome
items. To sort it, we need an
undefinedundefinedordering function that knows how to
compare its elements. The default is a string
order.undefinedundefined
The
undefinedundefinedarr.sort(fn) method implements a
generic sorting algorithm. We don't need to care how it
internally works (an optimized undefinedundefinedquicksort
or undefinedundefinedTimsort most
of the time). It will walk the array, compare its elements using
the provided function and reorder them, all we need is to
provide the undefinedundefinedfn which does the
comparison.undefinedundefined
By the way, if we ever want to know which elements are compared - nothing prevents from alerting them:
undefinedundefined
undefinedundefinedjs run [1, -2, 15, 2, 0, 8].sort(function(a, b) { alert( a + " <> " + b ); return a - b; });undefinedundefined
The algorithm may compare an element with multiple others in the process, but it tries to make as few comparisons as possible.
undefinedundefinedsmart header="A comparison function may return any number" Actually, a comparison function is only required to return a positive number to say "greater" and a negative number to say "less".
undefinedundefinedThat allows to write shorter functions:
undefinedundefinedrun let arr = [ 1, 2, 15 ];
undefinedundefinedarr.sort(function(a, b) { return a - b; });
undefinedundefinedalert(arr); // undefinedundefined!1, 2, 15undefinedundefined/!undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefinedsmart header="Arrow functions for the best" Remember undefinedundefinedarrow functions? We can use them here for neater sorting:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedarr.undefinedundefinedsort( (aundefinedundefined, b) undefinedundefined=> a undefinedundefined- b )undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
This works exactly the same as the longer version above.
undefinedundefined
undefinedundefinedsmart header="UselocaleCompare`
for strings" Remember undefinedundefinedstrings comparison
algorithm? It compares letters by their codes by
default.undefinedundefined
For many
alphabets, it's better to use
undefinedundefinedstr.localeCompare method to
correctly sort letters, such as
undefinedundefinedÖ.undefinedundefined
For example, let's sort a few countries in German:
undefinedundefinedrun let countries = [‘Österreich', ‘Andorra', ‘Vietnam'];
undefinedundefinedalert( countries.sort( (a, b) => a > b ? 1 : -1) ); // Andorra, Vietnam, Österreich (wrong)
undefinedundefinedalert( countries.sort( (a, b) => a.localeCompare(b) ) ); // Andorra,Österreich,Vietnam (correct!)
undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefinedThe method undefinedundefinedarr.reverse reverses the order
of elements in
undefinedundefinedarr.undefinedundefined
For instance:
undefinedundefinedrun let arr = [1, 2, 3, 4, 5]; arr.reverse();
undefinedundefinedalert( arr ); // 5,4,3,2,1
undefinedundefinedIt also returns the array
undefinedundefinedarr after the
reversal.undefinedundefined
Here's the situation from real life. We are writing a messaging
app, and the person enters the comma-delimited list of
receivers: undefinedundefinedJohn, Pete, Mary. But
for us an array of names would be much more comfortable than a
single string. How to get it?undefinedundefined
The undefinedundefinedstr.split(delim) method does
exactly that. It splits the string into an array by the given
delimiter
undefinedundefineddelim.undefinedundefined
In the example below, we split by a comma followed by space:
undefinedundefinedrun let names = ‘Bilbo, Gandalf, Nazgul';
undefinedundefinedlet arr = names.split(‘,''');
undefinedundefinedfor (let name of
arr) { alert(
undefinedundefinedA message to ${name}. ); // A
message to Bilbo (and other names) }
undefinedundefined
The
undefinedundefinedsplit method has an optional
second numeric argument - a limit on the array length. If it is
provided, then the extra elements are ignored. In practice it is
rarely used though:undefinedundefined
run let arr = ‘Bilbo, Gandalf, Nazgul, Saruman'.split(‘,''', 2);
undefinedundefinedalert(arr); // Bilbo, Gandalf
undefinedundefined
undefinedundefinedsmart header="Split into letters" The call tosplit(s)undefinedundefinedwith an emptys`
would split the string into an array of
letters:undefinedundefined
run let str = "test";
undefinedundefinedalert( str.split('') ); // t,e,s,t
undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefinedThe call undefinedundefinedarr.join(glue) does the reverse
to undefinedundefinedsplit. It creates a string of
undefinedundefinedarr items joined by
undefinedundefinedglue between
them.undefinedundefined
For instance:
undefinedundefinedrun let arr = [‘Bilbo', ‘Gandalf', ‘Nazgul'];
undefinedundefinedlet str = arr.join(‘;'''); // glue the array into a string using ;
undefinedundefinedalert( str ); // Bilbo;Gandalf;Nazgul
undefinedundefinedWhen we need to
iterate over an array - we can use
undefinedundefinedforEach,
undefinedundefinedfor or
undefinedundefinedfor..of.undefinedundefined
When we need to iterate and return the data
for each element - we can use
undefinedundefinedmap.undefinedundefined
The methods undefinedundefinedarr.reduce and undefinedundefinedarr.reduceRight also belong to that breed, but are a little bit more intricate. They are used to calculate a single value based on the array.undefinedundefined
undefinedundefinedThe syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet value undefinedundefined=undefinedundefinedarr.undefinedundefinedreduce(undefinedundefinedfunction(accumulatorundefinedundefined, itemundefinedundefined, indexundefinedundefined, array) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined}, [initial])undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The function is applied to all array elements one after another and "carries on" its result to the next call.
undefinedundefinedArguments:
undefinedundefinedaccumulator - is the result of
the previous function call, equals
undefinedundefinedinitial the first time (if
undefinedundefinedinitial is
provided).undefinedundefineditem - is the current array
item.undefinedundefinedindex - is its
position.undefinedundefinedarray - is the
array.undefinedundefinedAs function is applied, the result of the previous function call is passed to the next one as the first argument.
undefinedundefinedSo, the first argument is
essentially the accumulator that stores the combined result of
all previous executions. And at the end it becomes the result of
undefinedundefinedreduce.undefinedundefined
Sounds complicated?
undefinedundefinedThe easiest way to grasp that is by example.
undefinedundefinedHere we get a sum of an array in one line:
undefinedundefinedrun let arr = [1, 2, 3, 4, 5];
undefinedundefinedlet result = arr.reduce((sum, current) => sum + current, 0);
undefinedundefinedalert(result); // 15
undefinedundefinedThe function passed to
undefinedundefinedreduce uses only 2 arguments,
that's typically enough.undefinedundefined
Let's see the details of what's going on.
undefinedundefinedsum is the
undefinedundefinedinitial value (the last
argument of undefinedundefinedreduce), equals
undefinedundefined0, and
undefinedundefinedcurrent is the first array
element, equals undefinedundefined1. So the
function result is
undefinedundefined1.undefinedundefinedsum = 1, we add the second
array element (undefinedundefined2) to it and
return.undefinedundefinedsum = 3 and we add one
more element to it, and so on…undefinedundefinedThe calculation flow:
undefinedundefinedundefinedundefinedundefinedundefined
Or in the form of a table, where each row represents a function call on the next array element:
undefinedundefined| undefinedundefined |
undefinedundefinedsumundefinedundefined |
undefinedundefined
undefinedundefinedcurrentundefinedundefined
| undefinedundefinedresult | undefinedundefined
|---|---|---|---|
| the first call | undefinedundefined
undefinedundefined0undefinedundefined |
undefinedundefined
undefinedundefined1undefinedundefined |
undefinedundefined
undefinedundefined1undefinedundefined |
undefinedundefined
| the second call | undefinedundefinedundefinedundefined1undefinedundefined |
undefinedundefined
undefinedundefined2undefinedundefined |
undefinedundefined
undefinedundefined3undefinedundefined |
undefinedundefined
| the third call | undefinedundefinedundefinedundefined3undefinedundefined |
undefinedundefined
undefinedundefined3undefinedundefined |
undefinedundefined
undefinedundefined6undefinedundefined |
undefinedundefined
| the fourth call | undefinedundefinedundefinedundefined6undefinedundefined |
undefinedundefined
undefinedundefined4undefinedundefined |
undefinedundefined
undefinedundefined10undefinedundefined |
undefinedundefined
| the fifth call | undefinedundefinedundefinedundefined10undefinedundefined |
undefinedundefined
undefinedundefined5undefinedundefined |
undefinedundefined
undefinedundefined15undefinedundefined |
undefinedundefined
Here we can clearly see how the result of the previous call becomes the first argument of the next one.
undefinedundefinedWe also can omit the initial value:
undefinedundefinedrun let arr = [1, 2, 3, 4, 5];
undefinedundefined// removed initial value from reduce (no 0) let result = arr.reduce((sum, current) => sum + current);
undefinedundefinedalert( result ); // 15
undefinedundefinedThe result is the same. That's because if
there's no initial, then undefinedundefinedreduce
takes the first element of the array as the initial value and
starts the iteration from the 2nd element.undefinedundefined
The calculation table is the same as above, minus the first row.
undefinedundefinedBut such use
requires an extreme care. If the array is empty, then
undefinedundefinedreduce call without initial value
gives an error.undefinedundefined
Here's an example:
undefinedundefinedrun let arr = [];
undefinedundefined// Error: Reduce of empty array with no initial value // if the initial value existed, reduce would return it for the empty arr. arr.reduce((sum, current) => sum + current);
undefinedundefinedSo it's advised to always specify the initial value.
undefinedundefinedThe method undefinedundefinedarr.reduceRight does the same, but goes from right to left.undefinedundefined
undefinedundefinedArrays do not form a separate language type. They are based on objects.
undefinedundefinedSo
undefinedundefinedtypeof does not help to
distinguish a plain object from an array:undefinedundefined
undefinedundefinedjs run alert(typeof {}); // object alert(typeof []); // sameundefinedundefined
…But arrays are used so often that
there's a special method for that: undefinedundefinedArray.isArray(value). It
returns undefinedundefinedtrue if the
undefinedundefinedvalue is an array, and
undefinedundefinedfalse
otherwise.undefinedundefined
run alert(Array.isArray({})); // false
undefinedundefinedalert(Array.isArray([])); // true
undefinedundefinedAlmost all
array methods that call functions - like
undefinedundefinedfind,
undefinedundefinedfilter,
undefinedundefinedmap, with a notable exception of
undefinedundefinedsort, accept an optional
additional parameter
undefinedundefinedthisArg.undefinedundefined
That parameter is not explained in the sections above, because it's rarely used. But for completeness we have to cover it.
undefinedundefinedHere's the full syntax of these methods:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedarr.undefinedundefinedfind(funcundefinedundefined, thisArg)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedarr.undefinedundefinedfilter(funcundefinedundefined, thisArg)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedarr.undefinedundefinedmap(funcundefinedundefined, thisArg)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined// thisArg is the optional last argumentundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The value of
undefinedundefinedthisArg parameter becomes
undefinedundefinedthis for
undefinedundefinedfunc.undefinedundefined
For example, here we use a method of
undefinedundefinedarmy object as a filter, and
undefinedundefinedthisArg passes the
context:undefinedundefined
run let army = { minAge: 18, maxAge: 27, canJoin(user) { return user.age >= this.minAge && user.age < this.maxAge; } };
undefinedundefinedlet users = [ {age: 16}, {age: 20}, {age: 23}, {age: 30}];
undefinedundefinedundefinedundefined! // find users, for who army.canJoin returns true let soldiers = users.filter(army.canJoin, army); undefinedundefined/!undefinedundefined
undefinedundefinedalert(soldiers.length); // 2 alert(soldiers[0].age); // 20 alert(soldiers[1].age); // 23
undefinedundefinedIf in the example above we used
undefinedundefinedusers.filter(army.canJoin), then
undefinedundefinedarmy.canJoin would be called as a
standalone function, with
undefinedundefinedthis=undefined, thus leading to
an instant error.undefinedundefined
A
call to
undefinedundefinedusers.filter(army.canJoin, army)
can be replaced with
undefinedundefinedusers.filter(user => army.canJoin(user)),
that does the same. The latter is used more often, as it's a bit
easier to understand for most people.undefinedundefined
A cheat sheet of array methods:
undefinedundefinedpush(...items) - adds items
to the end,undefinedundefinedpop() - extracts an item
from the end,undefinedundefinedshift() - extracts an item
from the beginning,undefinedundefinedunshift(...items) - adds
items to the beginning.undefinedundefinedsplice(pos, deleteCount, ...items)
- at index undefinedundefinedpos deletes
undefinedundefineddeleteCount elements and
inserts
undefinedundefineditems.undefinedundefined
slice(start, end) - creates
a new array, copies elements from index
undefinedundefinedstart till
undefinedundefinedend (not inclusive) into
it.undefinedundefinedconcat(...items) - returns
a new array: copies all members of the current one and
adds undefinedundefineditems to it. If any of
undefinedundefineditems is an array, then its
elements are taken.undefinedundefinedindexOf/lastIndexOf(item, pos)
- look for undefinedundefineditem starting
from position undefinedundefinedpos, return
the index or undefinedundefined-1 if not
found.undefinedundefinedincludes(value) - returns
undefinedundefinedtrue if the array has
undefinedundefinedvalue, otherwise
undefinedundefinedfalse.undefinedundefined
find/filter(func) - filter
elements through the function, return first/all values
that make it return
undefinedundefinedtrue.undefinedundefined
findIndex is like
undefinedundefinedfind, but returns the index
instead of a value.undefinedundefinedforEach(func) - calls
undefinedundefinedfunc for every element,
does not return anything.undefinedundefinedmap(func) - creates a new
array from results of calling
undefinedundefinedfunc for every
element.undefinedundefinedsort(func) - sorts the
array in-place, then returns it.undefinedundefinedreverse() - reverses the
array in-place, then returns it.undefinedundefinedsplit/join - convert a
string to array and back.undefinedundefinedreduce/reduceRight(func, initial)
- calculate a single value over the array by calling
undefinedundefinedfunc for each element and
passing an intermediate result between the
calls.undefinedundefinedArray.isArray(arr) checks
undefinedundefinedarr for being an
array.undefinedundefinedPlease note that methods
undefinedundefinedsort,
undefinedundefinedreverse and
undefinedundefinedsplice modify the array
itself.undefinedundefined
These methods are the most used ones, they cover 99% of use cases. But there are few others:
undefinedundefinedundefinedundefinedarr.some(fn)/undefinedundefinedarr.every(fn) check the array.undefinedundefined
undefinedundefinedThe
function undefinedundefinedfn is called on each
element of the array similar to
undefinedundefinedmap. If any/all results are
undefinedundefinedtrue, returns
undefinedundefinedtrue, otherwise
undefinedundefinedfalse.undefinedundefined
These methods behave sort of like
undefinedundefined|| and
undefinedundefined&& operators: if
undefinedundefinedfn returns a truthy value,
undefinedundefinedarr.some() immediately
returns undefinedundefinedtrue and stops
iterating over the rest of items; if
undefinedundefinedfn returns a falsy value,
undefinedundefinedarr.every() immediately
returns undefinedundefinedfalse and stops
iterating over the rest of items as well.undefinedundefined
We can use
undefinedundefinedevery to compare arrays: run
function arraysEqual(arr1, arr2) { return arr1.length ===
arr2.length && arr1.every((value, index) => value
=== arr2[index]); }undefinedundefined
alert( arraysEqual([1, 2], [1, 2])); // true
undefinedundefinedundefinedundefinedarr.fill(value, start, end) -
fills the array with repeating
undefinedundefinedvalue from index
undefinedundefinedstart to
undefinedundefinedend.undefinedundefined
undefinedundefinedarr.copyWithin(target,
start, end) - copies its elements from position
undefinedundefinedstart till position
undefinedundefinedend into
undefinedundefineditself, at position
undefinedundefinedtarget (overwrites
existing).undefinedundefined
undefinedundefinedarr.flat(depth)/undefinedundefinedarr.flatMap(fn) create a new flat array from a multidimensional array.undefinedundefined
undefinedundefinedFor the full list, see the undefinedundefinedmanual.undefinedundefined
undefinedundefinedFrom the first sight it may seem that there are so many methods, quite difficult to remember. But actually that's much easier.
undefinedundefinedLook through the cheat sheet just to be aware of them. Then solve the tasks of this chapter to practice, so that you have experience with array methods.
undefinedundefinedAfterwards whenever you need to do something with an array, and you don't know how - come here, look at the cheat sheet and find the right method. Examples will help you to write it correctly. Soon you'll automatically remember the methods, without specific efforts from your side.
undefinedundefinedundefinedundefinedIterable objects
are a generalization of arrays. That's a concept that allows us
to make any object useable in a
undefinedundefinedfor..of loop.undefinedundefined
Of course, Arrays are iterable. But there are many other built-in objects, that are iterable as well. For instance, strings are also iterable.
undefinedundefinedIf
an object isn't technically an array, but represents a
collection (list, set) of something, then
undefinedundefinedfor..of is a great syntax to loop
over it, so let's see how to make it work.undefinedundefined
We can easily grasp the concept of iterables by making one of our own.
undefinedundefinedFor instance,
we have an object that is not an array, but looks suitable for
undefinedundefinedfor..of.undefinedundefined
Like a undefinedundefinedrange
object that represents an interval of numbers:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet range undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedfromundefinedundefined:undefinedundefined1undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedtoundefinedundefined:undefinedundefined5undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// We want the for..of to work:undefinedundefinedundefinedundefinedundefinedundefined// for(let num of range) ... num=1,2,3,4,5undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
To make the
undefinedundefinedrange object iterable (and thus
let undefinedundefinedfor..of work) we need to add
a method to the object named
undefinedundefinedSymbol.iterator (a special
built-in symbol just for that).undefinedundefined
for..of starts, it calls that
method once (or errors if not found). The method must return
an undefinedundefinediterator - an object with the
method undefinedundefinednext.undefinedundefined
for..of works
undefinedundefinedonly with that returned
object.undefinedundefinedfor..of wants the next
value, it calls undefinedundefinednext() on that
object.undefinedundefinednext() must have the form
undefinedundefined{done: Boolean, value: any},
where undefinedundefineddone=true means that the
iteration is finished, otherwise
undefinedundefinedvalue is the next
value.undefinedundefinedHere's the full implementation for
undefinedundefinedrange with
remarks:undefinedundefined
run let range = { from: 1, to: 5 };
undefinedundefined// 1. call to for..of initially calls this rangeundefinedundefinedSymbol.iterator = function() {undefinedundefined
undefinedundefined// …it returns the iterator object: // 2. Onward, for..of works only with this iterator, asking it for next values return { current: this.from, last: this.to,
undefinedundefinedundefinedundefined// 3. next() is called on each iteration by the for..of loop
next() {
// 4. it should return the value as an object {done:.., value :...}
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}undefinedundefinedundefinedundefined}; };
undefinedundefined// now it works! for (let num of range) { alert(num); // 1, then 2, 3, 4, 5 }
undefinedundefinedPlease note the core feature of iterables: separation of concerns.
undefinedundefinedrange
itself does not have the undefinedundefinednext()
method.undefinedundefinedrange[Symbol.iterator](),
and its undefinedundefinednext() generates values
for the iteration.undefinedundefinedSo, the iterator object is separate from the object it iterates over.
undefinedundefined
Technically, we may merge them and use
undefinedundefinedrange itself as the iterator to
make the code simpler.undefinedundefined
Like this:
undefinedundefinedrun let range = { from: 1, to: 5,
undefinedundefinedundefinedundefinedSymbol.iterator { this.current = this.from; return this; },undefinedundefined
undefinedundefinednext() { if (this.current <= this.to) { return { done: false, value: this.current++ }; } else { return { done: true }; } } };
undefinedundefinedfor (let num of range) { alert(num); // 1, then 2, 3, 4, 5 }
undefinedundefinedNow
undefinedundefinedrange[Symbol.iterator]() returns
the undefinedundefinedrange object itself: it has
the necessary undefinedundefinednext() method and
remembers the current iteration progress in
undefinedundefinedthis.current. Shorter? Yes. And
sometimes that's fine too.undefinedundefined
The downside is that now it's impossible to
have two undefinedundefinedfor..of loops running
over the object simultaneously: they'll share the iteration
state, because there's only one iterator - the object itself.
But two parallel for-ofs is a rare thing, even in async
scenarios.undefinedundefined
``undefinedundefinedsmart header="Infinite iterators" Infinite iterators are also possible. For instance, therangeundefinedundefinedbecomes infinite forrange.to
= Infinity`. Or we can make an iterable object that generates an
infinite sequence of pseudorandom numbers. Also can be
useful.undefinedundefined
There are no
limitations on undefinedundefinednext, it can
return more and more values, that's normal.undefinedundefined
Of course, the
undefinedundefinedfor..of loop over such an
iterable would be endless. But we can always stop it using
undefinedundefinedbreak.
undefinedundefined
Arrays and strings are most widely used built-in iterables.
undefinedundefinedFor a string,
undefinedundefinedfor..of loops over its
characters:undefinedundefined
undefinedundefinedjs run for (let char of "test") { // triggers 4 times: once for each character alert( char ); // t, then e, then s, then t }undefinedundefined
And it works correctly with surrogate pairs!
undefinedundefined
undefinedundefinedjs run let str = '𝒳😂'; for (let char of str) { alert( char ); // 𝒳, and then 😂 }undefinedundefined
For deeper understanding, let's see how to use an iterator explicitly.
undefinedundefinedWe'll iterate over a string
in exactly the same way as
undefinedundefinedfor..of, but with direct calls.
This code creates a string iterator and gets values from it
"manually":undefinedundefined
run let str = "Hello";
undefinedundefined// does the same as // for (let char of str) alert(char);
undefinedundefinedundefinedundefined! let iterator = strundefinedundefinedSymbol.iterator; undefinedundefined/!undefinedundefined
undefinedundefinedwhile (true) { let result = iterator.next(); if (result.done) break; alert(result.value); // outputs characters one by one }
undefinedundefinedThat is rarely needed, but gives us more
control over the process than
undefinedundefinedfor..of. For instance, we can
split the iteration process: iterate a bit, then stop, do
something else, and then resume later.undefinedundefined
Two official terms look similar, but are very different. Please make sure you understand them well to avoid the confusion.
undefinedundefinedSymbol.iterator
method, as described above.undefinedundefinedlength, so they look like
arrays.undefinedundefinedWhen we use JavaScript for practical tasks in a browser or any other environment, we may meet objects that are iterables or array-likes, or both.
undefinedundefinedFor
instance, strings are both iterable
(undefinedundefinedfor..of works on them) and
array-like (they have numeric indexes and
undefinedundefinedlength).undefinedundefined
But an iterable may be not array-like. And vice versa an array-like may be not iterable.
undefinedundefinedFor example, the
undefinedundefinedrange in the example above is
iterable, but not array-like, because it does not have indexed
properties and
undefinedundefinedlength.undefinedundefined
And here's the object that is array-like, but not iterable:
undefinedundefinedrun let arrayLike = { // has indexes and length => array-like 0: "Hello", 1: "World", length: 2 };
undefinedundefinedundefinedundefined! // Error (no Symbol.iterator) for (let item of arrayLike) {} undefinedundefined/! undefinedundefined
undefinedundefinedBoth iterables and
array-likes are usually undefinedundefinednot arrays,
they don't have undefinedundefinedpush,
undefinedundefinedpop etc. That's rather
inconvenient if we have such an object and want to work with it
as with an array. E.g. we would like to work with
undefinedundefinedrange using array methods. How to
achieve that?undefinedundefined
There's a
universal method undefinedundefinedArray.from that takes an iterable
or array-like value and makes a "real"
undefinedundefinedArray from it. Then we can call
array methods on it.undefinedundefined
For instance:
undefinedundefinedrun let arrayLike = { 0: "Hello", 1: "World", length: 2 };
undefinedundefinedundefinedundefined! let arr = Array.from(arrayLike); // (undefinedundefined) /!* alert(arr.pop()); // World (method works) undefinedundefined
undefinedundefined
undefinedundefinedArray.from at the line
undefinedundefined(*) takes the object, examines it
for being an iterable or array-like, then makes a new array and
copies all items to it.undefinedundefined
The same happens for an iterable:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// assuming that range is taken from the example aboveundefinedundefinedundefinedundefinedundefinedundefinedlet arr undefinedundefined=undefinedundefinedArray.undefinedundefinedfrom(range)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedalert(arr)undefinedundefined;undefinedundefined// 1,2,3,4,5 (array toString conversion works)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The full syntax for
undefinedundefinedArray.from also allows us to
provide an optional "mapping" function:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedArray.undefinedundefinedfrom(obj[undefinedundefined, mapFnundefinedundefined, thisArg])undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The optional second argument
undefinedundefinedmapFn can be a function that will
be applied to each element before adding it to the array, and
undefinedundefinedthisArg allows us to set
undefinedundefinedthis for it.undefinedundefined
For instance:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// assuming that range is taken from the example aboveundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// square each numberundefinedundefinedundefinedundefinedundefinedundefinedlet arr undefinedundefined=undefinedundefinedArray.undefinedundefinedfrom(rangeundefinedundefined, num undefinedundefined=> num undefinedundefined* num)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedalert(arr)undefinedundefined;undefinedundefined// 1,4,9,16,25undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Here we use
undefinedundefinedArray.from to turn a string into
an array of characters:undefinedundefined
run let str = ‘𝒳😂';
undefinedundefined// splits str into array of characters let chars = Array.from(str);
undefinedundefinedalert(chars[0]); // 𝒳 alert(chars[1]); // 😂 alert(chars.length); // 2
undefinedundefinedUnlike
undefinedundefinedstr.split, it relies on the
iterable nature of the string and so, just like
undefinedundefinedfor..of, correctly works with
surrogate pairs.undefinedundefined
Technically here it does the same as:
undefinedundefinedrun let str = ‘𝒳😂';
undefinedundefinedlet chars = []; // Array.from internally does the same loop for (let char of str) { chars.push(char); }
undefinedundefinedalert(chars);
undefinedundefined…But it is shorter.
undefinedundefinedWe can even build surrogate-aware
undefinedundefinedslice on it:undefinedundefined
run function slice(str, start, end) { return Array.from(str).slice(start, end).join(''); }
undefinedundefinedlet str = ‘𝒳😂𩷶';
undefinedundefinedalert( slice(str, 1, 3) ); // 😂𩷶
undefinedundefined// the native method does not support surrogate pairs alert( str.slice(1, 3) ); // garbage (two pieces from different surrogate pairs)
undefinedundefinedObjects that can be used in
undefinedundefinedfor..of are called
undefinedundefinediterable.undefinedundefined
Symbol.iterator.
undefinedundefinedobj[Symbol.iterator]() is
called an undefinedundefinediterator. It handles
further iteration process.undefinedundefinednext() that returns an
object
undefinedundefined{done: Boolean, value: any},
here undefinedundefineddone:true denotes the
end of the iteration process, otherwise the
undefinedundefinedvalue is the next
value.undefinedundefinedSymbol.iterator method is
called automatically by
undefinedundefinedfor..of, but we also can do it
directly.undefinedundefinedSymbol.iterator.undefinedundefined
Objects
that have indexed properties and
undefinedundefinedlength are called
undefinedundefinedarray-like. Such objects may also
have other properties and methods, but lack the built-in methods
of arrays.undefinedundefined
If we look inside the specification - we'll see that most built-in methods assume that they work with iterables or array-likes instead of "real" arrays, because that's more abstract.
undefinedundefined
undefinedundefinedArray.from(obj[, mapFn, thisArg])
makes a real undefinedundefinedArray from an
iterable or array-like undefinedundefinedobj, and
we can then use array methods on it. The optional arguments
undefinedundefinedmapFn and
undefinedundefinedthisArg allow us to apply a
function to each item.undefinedundefined
Till now, we've learned about the following complex data structures:
undefinedundefinedBut that's not
enough for real life. That's why
undefinedundefinedMap and
undefinedundefinedSet also exist.undefinedundefined
undefinedundefinedMap is a collection
of keyed data items, just like an
undefinedundefinedObject. But the main difference
is that undefinedundefinedMap allows keys of any
type.undefinedundefined
Methods and properties are:
undefinedundefinednew Map() - creates the
map.undefinedundefinedmap.set(key, value) - stores
the value by the key.undefinedundefinedmap.get(key) - returns the
value by the key, undefinedundefinedundefined if
undefinedundefinedkey doesn't exist in
map.undefinedundefinedmap.has(key) - returns
undefinedundefinedtrue if the
undefinedundefinedkey exists,
undefinedundefinedfalse
otherwise.undefinedundefinedmap.delete(key) - removes the
value by the key.undefinedundefinedmap.clear() - removes
everything from the map.undefinedundefinedmap.size -
returns the current element count.undefinedundefinedFor instance:
undefinedundefinedrun let map = new Map();
undefinedundefinedmap.set(‘1', ‘str1'); // a string key map.set(1, ‘num1'); // a numeric key map.set(true, ‘bool1'); // a boolean key
undefinedundefined// remember the regular Object? it would convert keys to string // Map keeps the type, so these two are different: alert( map.get(1) ); // ‘num1' alert( map.get(‘1') ); // ‘str1'
undefinedundefinedalert( map.size ); // 3
undefinedundefinedAs we can see, unlike objects, keys are not converted to strings. Any type of key is possible.
undefinedundefined
``undefinedundefinedsmart header="map[key]undefinedundefinedisn't the right way to use aMapundefinedundefined" Althoughmap[key]undefinedundefinedalso works, e.g. we can setmap[key]
= 2undefinedundefined, this is treatingmap` as a
plain JavaScript object, so it implies all corresponding
limitations (only string/symbol keys and so
on).undefinedundefined
So we should use
undefinedundefinedmap methods:
undefinedundefinedset,
undefinedundefinedget and so on.
undefinedundefined
undefinedundefinedMap can also use objects as keys.undefinedundefined
undefinedundefinedFor instance:
undefinedundefinedrun let john = { name: "John" };
undefinedundefined// for every user, let's store their visits count let visitsCountMap = new Map();
undefinedundefined// john is the key for the map visitsCountMap.set(john, 123);
undefinedundefinedalert( visitsCountMap.get(john) ); // 123
undefinedundefinedUsing objects as keys is one of the most
notable and important undefinedundefinedMap
features. The same does not count for
undefinedundefinedObject. String as a key in
undefinedundefinedObject is fine, but we can't use
another undefinedundefinedObject as a key in
undefinedundefinedObject.undefinedundefined
Let's try:
undefinedundefinedrun let john = { name: "John" }; let ben = { name: "Ben" };
undefinedundefinedlet visitsCountObj = {}; // try to use an object
undefinedundefinedvisitsCountObj[ben] = 234; // try to use ben object as the key visitsCountObj[john] = 123; // try to use john object as the key, ben object will get replaced
undefinedundefinedundefinedundefined! // That's what got written! alert( visitsCountObj["[object Object]"] ); // 123 undefinedundefined/! undefinedundefined
undefinedundefinedAs
undefinedundefinedvisitsCountObj is an object, it
converts all undefinedundefinedObject keys, such as
undefinedundefinedjohn and
undefinedundefinedben above, to same string
undefinedundefined"[object Object]". Definitely not
what we want.undefinedundefined
``undefinedundefinedsmart header="HowMapundefinedundefinedcompares keys" To test keys for equivalence,Mapundefinedundefineduses the algorithm [SameValueZero](https://tc39.github.io/ecma262/#sec-samevaluezero). It is roughly the same as strict equality===undefinedundefined, but the difference is thatNaNundefinedundefinedis considered equal toNaNundefinedundefined. SoNaN`
can be used as the key as well.undefinedundefined
This algorithm can't be changed or customized.
undefinedundefined
undefinedundefinedsmart header="Chaining" Everymap.set`
call returns the map itself, so we can "chain" the
calls:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedmap.undefinedundefinedset(undefinedundefined'1'undefinedundefined,undefinedundefined'str1')undefinedundefinedundefinedundefined .undefinedundefinedset(undefinedundefined1undefinedundefined,undefinedundefined'num1')undefinedundefinedundefinedundefined .undefinedundefinedset(undefinedundefinedtrueundefinedundefined,undefinedundefined'bool1')undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefined
For looping over a
undefinedundefinedmap, there are 3
methods:undefinedundefined
map.keys()
- returns an iterable for keys,undefinedundefinedmap.values() - returns an
iterable for values,undefinedundefinedmap.entries() - returns an
iterable for entries
undefinedundefined[key, value], it's used by
default in
undefinedundefinedfor..of.undefinedundefinedFor instance:
undefinedundefinedrun let recipeMap = new Map([ [‘cucumber', 500], [‘tomatoes', 350], [‘onion', 50]]);
undefinedundefined// iterate over keys (vegetables) for (let vegetable of recipeMap.keys()) { alert(vegetable); // cucumber, tomatoes, onion }
undefinedundefined// iterate over values (amounts) for (let amount of recipeMap.values()) { alert(amount); // 500, 350, 50 }
undefinedundefined// iterate over [key, value] entries for (let entry of recipeMap) { // the same as of recipeMap.entries() alert(entry); // cucumber,500 (and so on) }
undefinedundefined
undefinedundefinedsmart header="The insertion order is used" The iteration goes in the same order as the values were inserted. `Map` preserves this order, unlike a regular `Object`.undefinedundefined
Besides that,
undefinedundefinedMap has a built-in
undefinedundefinedforEach method, similar to
undefinedundefinedArray:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// runs the function for each (key, value) pairundefinedundefinedundefinedundefinedundefinedundefinedrecipeMap.undefinedundefinedforEach( (valueundefinedundefined, keyundefinedundefined, map) undefinedundefined=>undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`undefinedundefined${keyundefinedundefined}undefinedundefined: undefinedundefined${valueundefinedundefined}undefinedundefined`)undefinedundefined;undefinedundefined// cucumber: 500 etcundefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
When a
undefinedundefinedMap is created, we can pass an
array (or another iterable) with key/value pairs for
initialization, like this:undefinedundefined
run // array of [key, value] pairs let map = new Map([ [‘1', ‘str1'], [1, ‘num1'], [true, ‘bool1']]);
undefinedundefinedalert( map.get(‘1') ); // str1
undefinedundefinedIf we have a plain object, and we'd like
to create a undefinedundefinedMap from it, then we
can use built-in method undefinedundefinedObject.entries(obj) that
returns an array of key/value pairs for an object exactly in
that format.undefinedundefined
So we can create a map from an object like this:
undefinedundefinedrun let obj = { name: "John", age: 30 };
undefinedundefinedundefinedundefined! let map = new Map(Object.entries(obj)); undefinedundefined/!undefinedundefined
undefinedundefinedalert( map.get(‘name') ); // John
undefinedundefinedHere,
undefinedundefinedObject.entries returns the array
of key/value pairs:
undefinedundefined[ ["name","John"], ["age", 30] ].
That's what undefinedundefinedMap
needs.undefinedundefined
We've just seen how to
create undefinedundefinedMap from a plain object
with
undefinedundefinedObject.entries(obj).undefinedundefined
There's
undefinedundefinedObject.fromEntries method that
does the reverse: given an array of
undefinedundefined[key, value] pairs, it creates an
object from them:undefinedundefined
run let prices = Object.fromEntries([ [‘banana', 1], [‘orange', 2], [‘meat', 4]]);
undefinedundefined// now prices = { banana: 1, orange: 2, meat: 4 }
undefinedundefinedalert(prices.orange); // 2
undefinedundefinedWe can use
undefinedundefinedObject.fromEntries to get a plain
object from
undefinedundefinedMap.undefinedundefined
E.g. we store the data in a
undefinedundefinedMap, but we need to pass it to a
3rd-party code that expects a plain object.undefinedundefined
Here we go:
undefinedundefinedrun let map = new Map(); map.set(‘banana', 1); map.set(‘orange', 2); map.set(‘meat', 4);
undefinedundefinedundefinedundefined! let obj = Object.fromEntries(map.entries()); // make a plain object (undefinedundefined) /!*undefinedundefined
undefinedundefined// done! // obj = { banana: 1, orange: 2, meat: 4 }
undefinedundefinedalert(obj.orange); // 2
undefinedundefinedA call to
undefinedundefinedmap.entries() returns an iterable
of key/value pairs, exactly in the right format for
undefinedundefinedObject.fromEntries.undefinedundefined
We could also make line
undefinedundefined(*) shorter:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet obj undefinedundefined=undefinedundefinedObject.undefinedundefinedfromEntries(map)undefinedundefined;undefinedundefined// omit .entries()undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That's the same, because
undefinedundefinedObject.fromEntries expects an
iterable object as the argument. Not necessarily an array. And
the standard iteration for undefinedundefinedmap
returns same key/value pairs as
undefinedundefinedmap.entries(). So we get a plain
object with same key/values as the
undefinedundefinedmap.undefinedundefined
A
undefinedundefinedSet is a special type collection
- "set of values" (without keys), where each value may occur
only once.undefinedundefined
Its main methods are:
undefinedundefinednew Set(iterable) - creates the
set, and if an undefinedundefinediterable object
is provided (usually an array), copies values from it into the
set.undefinedundefinedset.add(value) - adds a value,
returns the set itself.undefinedundefinedset.delete(value) - removes the
value, returns undefinedundefinedtrue if
undefinedundefinedvalue existed at the moment of
the call, otherwise
undefinedundefinedfalse.undefinedundefinedset.has(value) - returns
undefinedundefinedtrue if the value exists in the
set, otherwise
undefinedundefinedfalse.undefinedundefinedset.clear()
- removes everything from the set.undefinedundefinedset.size -
is the elements count.undefinedundefinedThe main feature is
that repeated calls of
undefinedundefinedset.add(value) with the same
value don't do anything. That's the reason why each value
appears in a undefinedundefinedSet only
once.undefinedundefined
For example, we have visitors coming, and we'd like to remember everyone. But repeated visits should not lead to duplicates. A visitor must be "counted" only once.
undefinedundefined
undefinedundefinedSet is just the right thing for
that:undefinedundefined
run let set = new Set();
undefinedundefinedlet john = { name: "John" }; let pete = { name: "Pete" }; let mary = { name: "Mary" };
undefinedundefined// visits, some users come multiple times set.add(john); set.add(pete); set.add(mary); set.add(john); set.add(mary);
undefinedundefined// set keeps only unique values alert( set.size ); // 3
undefinedundefinedfor (let user of set) { alert(user.name); // John (then Pete and Mary) }
undefinedundefinedThe alternative to
undefinedundefinedSet could be an array of users,
and the code to check for duplicates on every insertion using
undefinedundefinedarr.find. But
the performance would be much worse, because this method walks
through the whole array checking every element.
undefinedundefinedSet is much better optimized
internally for uniqueness checks.undefinedundefined
We can loop over a set either with
undefinedundefinedfor..of or using
undefinedundefinedforEach:undefinedundefined
run let set = new Set(["oranges", "apples", "bananas"]);
undefinedundefinedfor (let value of set) alert(value);
undefinedundefined// the same with forEach: set.forEach((value, valueAgain, set) => { alert(value); });
undefinedundefinedNote the funny thing. The callback
function passed in undefinedundefinedforEach has 3
arguments: a undefinedundefinedvalue, then
undefinedundefinedthe same
valueundefinedundefinedvalueAgain, and then
the target object. Indeed, the same value appears in the
arguments twice.undefinedundefined
That's for compatibility with undefinedundefinedMap
where the callback passed undefinedundefinedforEach
has three arguments. Looks a bit strange, for sure. But may help
to replace undefinedundefinedMap with
undefinedundefinedSet in certain cases with ease,
and vice versa.undefinedundefined
The
same methods undefinedundefinedMap has for
iterators are also supported:undefinedundefined
set.keys() - returns an
iterable object for values,undefinedundefinedset.values() - same as
undefinedundefinedset.keys(), for compatibility
with undefinedundefinedMap,undefinedundefined
set.entries() - returns an
iterable object for entries
undefinedundefined[value, value], exists for
compatibility with
undefinedundefinedMap.undefinedundefined
undefinedundefinedMap - is a collection of keyed
values.undefinedundefined
Methods and properties:
undefinedundefinednew Map([iterable]) - creates
the map, with optional undefinedundefinediterable
(e.g. array) of undefinedundefined[key,value]
pairs for initialization.undefinedundefinedmap.set(key, value) - stores
the value by the key, returns the map
itself.undefinedundefinedmap.get(key) - returns the
value by the key, undefinedundefinedundefined if
undefinedundefinedkey doesn't exist in
map.undefinedundefinedmap.has(key) - returns
undefinedundefinedtrue if the
undefinedundefinedkey exists,
undefinedundefinedfalse
otherwise.undefinedundefinedmap.delete(key) - removes the
value by the key, returns undefinedundefinedtrue
if undefinedundefinedkey existed at the moment of
the call, otherwise
undefinedundefinedfalse.undefinedundefinedmap.clear()
- removes everything from the map.undefinedundefinedmap.size -
returns the current element count.undefinedundefinedThe differences from
a regular
undefinedundefinedObject:undefinedundefined
size
property.undefinedundefinedundefinedundefinedSet - is a
collection of unique values.undefinedundefined
Methods and properties:
undefinedundefinednew Set([iterable]) - creates
the set, with optional undefinedundefinediterable
(e.g. array) of values for initialization.undefinedundefined
set.add(value) - adds a value
(does nothing if undefinedundefinedvalue exists),
returns the set itself.undefinedundefinedset.delete(value) - removes the
value, returns undefinedundefinedtrue if
undefinedundefinedvalue existed at the moment of
the call, otherwise
undefinedundefinedfalse.undefinedundefinedset.has(value) - returns
undefinedundefinedtrue if the value exists in the
set, otherwise
undefinedundefinedfalse.undefinedundefinedset.clear()
- removes everything from the set.undefinedundefinedset.size -
is the elements count.undefinedundefinedIteration over
undefinedundefinedMap and
undefinedundefinedSet is always in the insertion
order, so we can't say that these collections are unordered, but
we can't reorder elements or directly get an element by its
number.undefinedundefined
As we know from the chapter undefinedundefinedinfo:garbage-collection, JavaScript engine keeps a value in memory while it is "reachable" and can potentially be used.undefinedundefined
undefinedundefinedFor instance:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet john undefinedundefined=undefinedundefined{undefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// the object can be accessed, john is the reference to itundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// overwrite the referenceundefinedundefinedundefinedundefinedjohn undefinedundefined=undefinedundefinednullundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefined// the object will be removed from memoryundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Usually, properties of an object or elements of an array or another data structure are considered reachable and kept in memory while that data structure is in memory.
undefinedundefinedFor instance, if we put an object into an array, then while the array is alive, the object will be alive as well, even if there are no other references to it.
undefinedundefinedLike this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet john undefinedundefined=undefinedundefined{undefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet array undefinedundefined= [ john ]undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedjohn undefinedundefined=undefinedundefinednullundefinedundefined;undefinedundefined// overwrite the referenceundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefined// the object previously referenced by john is stored inside the arrayundefinedundefinedundefinedundefinedundefinedundefined// therefore it won't be garbage-collectedundefinedundefinedundefinedundefinedundefinedundefined// we can get it as array[0]undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Similar to that, if we use an object as
the key in a regular undefinedundefinedMap, then
while the undefinedundefinedMap exists, that object
exists as well. It occupies memory and may not be garbage
collected.undefinedundefined
For instance:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet john undefinedundefined=undefinedundefined{undefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet map undefinedundefined=undefinedundefinednewundefinedundefinedMap()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedmap.undefinedundefinedset(johnundefinedundefined,undefinedundefined"...")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedjohn undefinedundefined=undefinedundefinednullundefinedundefined;undefinedundefined// overwrite the referenceundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefined// john is stored inside the map,undefinedundefinedundefinedundefinedundefinedundefined// we can get it by using map.keys()undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedWeakMap
is fundamentally different in this aspect. It doesn't prevent
garbage-collection of key objects.undefinedundefined
Let's see what it means on examples.
undefinedundefinedThe first difference between undefinedundefinedMap
and undefinedundefinedWeakMap is that keys must be
objects, not primitive values:undefinedundefined
run let weakMap = new WeakMap();
undefinedundefinedlet obj = {};
undefinedundefinedweakMap.set(obj, "ok"); // works fine (object key)
undefinedundefinedundefinedundefined! // can't use a string as the key weakMap.set("test", "Whoops"); // Error, because "test" is not an object undefinedundefined/! undefinedundefined
undefinedundefinedNow, if we use an object as the key in it, and there are no other references to that object - it will be removed from memory (and from the map) automatically.
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet john undefinedundefined=undefinedundefined{undefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet weakMap undefinedundefined=undefinedundefinednewundefinedundefinedWeakMap()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedweakMap.undefinedundefinedset(johnundefinedundefined,undefinedundefined"...")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedjohn undefinedundefined=undefinedundefinednullundefinedundefined;undefinedundefined// overwrite the referenceundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// john is removed from memory!undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Compare it with the regular
undefinedundefinedMap example above. Now if
undefinedundefinedjohn only exists as the key of
undefinedundefinedWeakMap - it will be
automatically deleted from the map (and
memory).undefinedundefined
undefinedundefinedWeakMap does not support
iteration and methods undefinedundefinedkeys(),
undefinedundefinedvalues(),
undefinedundefinedentries(), so there's no way to
get all keys or values from it.undefinedundefined
undefinedundefinedWeakMap has
only the following methods:undefinedundefined
weakMap.get(key)undefinedundefined
weakMap.set(key, value)undefinedundefined
weakMap.delete(key)undefinedundefined
weakMap.has(key)undefinedundefined
Why such a
limitation? That's for technical reasons. If an object has lost
all other references (like undefinedundefinedjohn
in the code above), then it is to be garbage-collected
automatically. But technically it's not exactly specified
undefinedundefinedwhen the cleanup
happens.undefinedundefined
The
JavaScript engine decides that. It may choose to perform the
memory cleanup immediately or to wait and do the cleaning later
when more deletions happen. So, technically, the current element
count of a undefinedundefinedWeakMap is not known.
The engine may have cleaned it up or not, or did it partially.
For that reason, methods that access all keys/values are not
supported.undefinedundefined
Now, where do we need such a data structure?
undefinedundefinedThe main area of application for
undefinedundefinedWeakMap is an
undefinedundefinedadditional data
storage.undefinedundefined
If
we're working with an object that "belongs" to another code,
maybe even a third-party library, and would like to store some
data associated with it, that should only exist while the object
is alive - then undefinedundefinedWeakMap is
exactly what's needed.undefinedundefined
We put the data to a undefinedundefinedWeakMap,
using the object as the key, and when the object is garbage
collected, that data will automatically disappear as
well.undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedweakMap.undefinedundefinedset(johnundefinedundefined,undefinedundefined"secret documents")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined// if john dies, secret documents will be destroyed automaticallyundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Let's look at an example.
undefinedundefinedFor instance, we have code that keeps a visit count for users. The information is stored in a map: a user object is the key and the visit count is the value. When a user leaves (its object gets garbage collected), we don't want to store their visit count anymore.
undefinedundefinedHere's
an example of a counting function with
undefinedundefinedMap:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 visitsCount.jsundefinedundefinedundefinedundefinedundefinedundefinedlet visitsCountMap undefinedundefined=undefinedundefinednewundefinedundefinedMap()undefinedundefined;undefinedundefined// map: user => visits countundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// increase the visits countundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedcountUser(user) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet count undefinedundefined=undefinedundefinedvisitsCountMap.undefinedundefinedget(user) undefinedundefined||undefinedundefined0undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedvisitsCountMap.undefinedundefinedset(userundefinedundefined, count undefinedundefined+undefinedundefined1)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
And here's another part of the code, maybe another file using it:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// 📁 main.jsundefinedundefinedundefinedundefinedundefinedundefinedlet john undefinedundefined=undefinedundefined{undefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedcountUser(john)undefinedundefined;undefinedundefined// count his visitsundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// later john leaves usundefinedundefinedundefinedundefinedjohn undefinedundefined=undefinedundefinednullundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Now,
undefinedundefinedjohn object should be garbage
collected, but remains in memory, as it's a key in
undefinedundefinedvisitsCountMap.undefinedundefined
We need to clean
undefinedundefinedvisitsCountMap when we remove
users, otherwise it will grow in memory indefinitely. Such
cleaning can become a tedious task in complex
architectures.undefinedundefined
We can
avoid it by switching to undefinedundefinedWeakMap
instead:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 visitsCount.jsundefinedundefinedundefinedundefinedundefinedundefinedlet visitsCountMap undefinedundefined=undefinedundefinednewundefinedundefinedWeakMap()undefinedundefined;undefinedundefined// weakmap: user => visits countundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// increase the visits countundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedcountUser(user) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet count undefinedundefined=undefinedundefinedvisitsCountMap.undefinedundefinedget(user) undefinedundefined||undefinedundefined0undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedvisitsCountMap.undefinedundefinedset(userundefinedundefined, count undefinedundefined+undefinedundefined1)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Now we don't have to clean
undefinedundefinedvisitsCountMap. After
undefinedundefinedjohn object becomes unreachable,
by all means except as a key of
undefinedundefinedWeakMap, it gets removed from
memory, along with the information by that key from
undefinedundefinedWeakMap.undefinedundefined
Another common example is caching. We can store ("cache") results from a function, so that future calls on the same object can reuse it.
undefinedundefinedTo achieve
that, we can use undefinedundefinedMap (not optimal
scenario):undefinedundefined
run // 📁 cache.js let cache = new Map();
undefinedundefined// calculate and remember the result function process(obj) { if (!cache.has(obj)) { let result = /* calculations of the result for */ obj;
undefinedundefinedundefinedundefinedcache.set(obj, result);undefinedundefined
undefinedundefined}
undefinedundefinedreturn cache.get(obj); }
undefinedundefinedundefinedundefined! // Now we use process() in another file: undefinedundefined/!undefinedundefined
undefinedundefined// 📁 main.js let obj = {/* let's say we have an object */};
undefinedundefinedlet result1 = process(obj); // calculated
undefinedundefined// …later, from another place of the code… let result2 = process(obj); // remembered result taken from cache
undefinedundefined// …later, when the object is not needed any more: obj = null;
undefinedundefinedalert(cache.size); // 1 (Ouch! The object is still in cache, taking memory!)
undefinedundefinedFor multiple calls of
undefinedundefinedprocess(obj) with the same
object, it only calculates the result the first time, and then
just takes it from undefinedundefinedcache. The
downside is that we need to clean
undefinedundefinedcache when the object is not
needed any more.undefinedundefined
If we
replace undefinedundefinedMap with
undefinedundefinedWeakMap, then this problem
disappears. The cached result will be removed from memory
automatically after the object gets garbage
collected.undefinedundefined
run // 📁 cache.js undefinedundefined! let cache = new WeakMap(); undefinedundefined/!undefinedundefined
undefinedundefined// calculate and remember the result function process(obj) { if (!cache.has(obj)) { let result = /* calculate the result for */ obj;
undefinedundefinedundefinedundefinedcache.set(obj, result);undefinedundefined
undefinedundefined}
undefinedundefinedreturn cache.get(obj); }
undefinedundefined// 📁 main.js let obj = {/* some object */};
undefinedundefinedlet result1 = process(obj); let result2 = process(obj);
undefinedundefined// …later, when the object is not needed any more: obj = null;
undefinedundefined// Can't get cache.size, as it's a WeakMap, // but it's 0 or soon be 0 // When obj gets garbage collected, cached data will be removed as well
undefinedundefinedundefinedundefinedWeakSet
behaves similarly:undefinedundefined
Set, but we may only add
objects to undefinedundefinedWeakSet (not
primitives).undefinedundefinedSet, it supports
undefinedundefinedadd,
undefinedundefinedhas and
undefinedundefineddelete, but not
undefinedundefinedsize,
undefinedundefinedkeys() and no
iterations.undefinedundefinedBeing "weak", it also serves as additional
storage. But not for arbitrary data, rather for "yes/no" facts.
A membership in undefinedundefinedWeakSet may mean
something about the object.undefinedundefined
For instance, we can add users to
undefinedundefinedWeakSet to keep track of those
who visited our site:undefinedundefined
run let visitedSet = new WeakSet();
undefinedundefinedlet john = { name: "John" }; let pete = { name: "Pete" }; let mary = { name: "Mary" };
undefinedundefinedvisitedSet.add(john); // John visited us visitedSet.add(pete); // Then Pete visitedSet.add(john); // John again
undefinedundefined// visitedSet has 2 users now
undefinedundefined// check if John visited? alert(visitedSet.has(john)); // true
undefinedundefined// check if Mary visited? alert(visitedSet.has(mary)); // false
undefinedundefinedjohn = null;
undefinedundefined// visitedSet will be cleaned automatically
undefinedundefinedThe most notable limitation of
undefinedundefinedWeakMap and
undefinedundefinedWeakSet is the absence of
iterations, and the inability to get all current content. That
may appear inconvenient, but does not prevent
undefinedundefinedWeakMap/WeakSet from doing their
main job - be an "additional" storage of data for objects which
are stored/managed at another place.undefinedundefined
undefinedundefinedWeakMap is
undefinedundefinedMap-like collection that allows
only objects as keys and removes them together with associated
value once they become inaccessible by other
means.undefinedundefined
undefinedundefinedWeakSet is
undefinedundefinedSet-like collection that stores
only objects and removes them once they become inaccessible by
other means.undefinedundefined
Their main advantages are that they have weak reference to objects, so they can easily be removed by garbage collector.
undefinedundefinedThat comes at the cost of not having support
for undefinedundefinedclear,
undefinedundefinedsize,
undefinedundefinedkeys,
undefinedundefinedvalues…undefinedundefined
undefinedundefinedWeakMap and
undefinedundefinedWeakSet are used as "secondary"
data structures in addition to the "primary" object storage.
Once the object is removed from the primary storage, if it is
only found as the key of undefinedundefinedWeakMap
or in a undefinedundefinedWeakSet, it will be
cleaned up automatically.undefinedundefined
Let's step away from the individual data structures and talk about the iterations over them.
undefinedundefinedIn the previous chapter we
saw methods undefinedundefinedmap.keys(),
undefinedundefinedmap.values(),
undefinedundefinedmap.entries().undefinedundefined
These methods are generic, there is a common agreement to use them for data structures. If we ever create a data structure of our own, we should implement them too.
undefinedundefinedThey are supported for:
undefinedundefinedMapundefinedundefinedSetundefinedundefinedArrayundefinedundefinedPlain objects also support similar methods, but the syntax is a bit different.
undefinedundefinedFor plain objects, the following methods are available:
undefinedundefined[key, value]
pairs.undefinedundefinedPlease note the distinctions (compared to map for example):
undefinedundefined| undefinedundefined | Map | undefinedundefinedObject | undefinedundefined
|---|---|---|
| Call syntax | undefinedundefined
undefinedundefinedmap.keys()undefinedundefined
| undefinedundefined
undefinedundefinedObject.keys(obj), but not
undefinedundefinedobj.keys()undefinedundefined
| undefinedundefined
| Returns | undefinedundefinediterable | undefinedundefined"real" Array | undefinedundefined
The first
difference is that we have to call
undefinedundefinedObject.keys(obj), and not
undefinedundefinedobj.keys().undefinedundefined
Why so? The main reason is flexibility.
Remember, objects are a base of all complex structures in
JavaScript. So we may have an object of our own like
undefinedundefineddata that implements its own
undefinedundefineddata.values() method. And we
still can call
undefinedundefinedObject.values(data) on
it.undefinedundefined
The second
difference is that undefinedundefinedObject.*
methods return "real" array objects, not just an iterable.
That's mainly for historical reasons.undefinedundefined
For instance:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedageundefinedundefined:undefinedundefined30undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Object.keys(user) = ["name", "age"]undefinedundefined
Object.values(user) = ["John", 30]undefinedundefined
Object.entries(user) = [ ["name","John"], ["age",30] ]undefinedundefined
Here's an
example of using undefinedundefinedObject.values to
loop over property values:undefinedundefined
run let user = { name: "John", age: 30 };
undefinedundefined// loop over values for (let value of Object.values(user)) { alert(value); // John, then 30 }
undefinedundefined
``undefinedundefinedwarn header="Object.keys/values/entries ignore symbolic properties" Just like afor..inundefinedundefinedloop, these methods ignore properties that useSymbol(…)`
as keys.undefinedundefined
Usually that's convenient. But if we want symbolic keys too, then there's a separate method undefinedundefinedObject.getOwnPropertySymbols that returns an array of only symbolic keys. Also, there exist a method undefinedundefinedReflect.ownKeys(obj) that returns undefinedundefinedall keys. undefinedundefined
undefinedundefinedObjects lack many methods that exist for
arrays, e.g. undefinedundefinedmap,
undefinedundefinedfilter and
others.undefinedundefined
If we'd like
to apply them, then we can use
undefinedundefinedObject.entries followed by
undefinedundefinedObject.fromEntries:undefinedundefined
Object.entries(obj) to get an
array of key/value pairs from
undefinedundefinedobj.undefinedundefinedmap.undefinedundefined
Object.fromEntries(array) on
the resulting array to turn it back into an
object.undefinedundefinedFor example, we have an object with prices, and would like to double them:
undefinedundefinedrun let prices = { banana: 1, orange: 2, meat: 4, };
undefinedundefinedundefinedundefined! let doublePrices = Object.fromEntries( // convert to array, map, and then fromEntries gives back the object Object.entries(prices).map(([key, value]) => [key, value * 2]) ); undefinedundefined/!undefinedundefined
undefinedundefinedalert(doublePrices.meat); // 8
undefinedundefinedIt may look difficult at first sight, but becomes easy to understand after you use it once or twice. We can make powerful chains of transforms this way.
undefinedundefinedThe two most used data
structures in JavaScript are
undefinedundefinedObject and
undefinedundefinedArray.undefinedundefined
Although, when we pass those to a function, it may need not an object/array as a whole. It may need individual pieces.
undefinedundefinedundefinedundefinedDestructuring assignment is a special syntax that allows us to "unpack" arrays or objects into a bunch of variables, as sometimes that's more convenient.undefinedundefined
undefinedundefinedDestructuring also works great with complex functions that have a lot of parameters, default values, and so on. Soon we'll see that.
undefinedundefinedHere's an example of how an array is destructured into variables:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// we have an array with the name and surnameundefinedundefinedundefinedundefinedundefinedundefinedlet arr undefinedundefined= [undefinedundefined"John"undefinedundefined,undefinedundefined"Smith"]undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefined// destructuring assignmentundefinedundefinedundefinedundefinedundefinedundefined// sets firstName = arr[0]undefinedundefinedundefinedundefinedundefinedundefined// and surname = arr[1]undefinedundefinedundefinedundefinedundefinedundefinedlet [firstNameundefinedundefined, surname] undefinedundefined= arrundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedalertundefinedundefined(undefinedundefinedfirstNameundefinedundefined)undefinedundefined; // Johnundefinedundefinedundefinedundefinedundefinedundefinedalertundefinedundefined(undefinedundefinedsurnameundefinedundefined)undefinedundefined; // Smithundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Now we can work with variables instead of array members.
undefinedundefinedIt looks great when
combined with undefinedundefinedsplit or other
array-returning methods:undefinedundefined
undefinedundefinedjs run let [firstName, surname] = "John Smith".split(' '); alert(firstName); // John alert(surname); // Smithundefinedundefined
As you can see, the syntax is simple. There are several peculiar details though. Let's see more examples, to better understand it.
undefinedundefinedsmart header=""Destructuring" does not mean "destructive"." It's called "destructuring assignment," because it "destructurizes" by copying items into variables. But the array itself is not modified.
undefinedundefinedIt's just a shorter way to write:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// let [firstName, surname] = arr;undefinedundefinedundefinedundefinedundefinedundefinedlet firstName undefinedundefined= arr[undefinedundefined0]undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedlet surname undefinedundefined= arr[undefinedundefined1]undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefined
smart header="Ignore elements using commas" Unwanted elements of the array can also be thrown away via an extra comma:
undefinedundefinedrun undefinedundefined! // second element is not needed let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; undefinedundefined/!undefinedundefined
undefinedundefinedalert( title ); // Consul
undefinedundefinedundefinedundefined
In the code above, the second element of the array is skipped, the third one is assigned to `title`, and the rest of the array items is also skipped (as there are no variables for them).undefinedundefined
undefinedundefinedsmart header="Works with any iterable on the right-side"
undefinedundefined…Actually, we can use it with any iterable, not only arrays:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet [aundefinedundefined, bundefinedundefined, c] undefinedundefined=undefinedundefined"abc"undefinedundefined;undefinedundefined// ["a", "b", "c"]undefinedundefinedundefinedundefinedundefinedundefinedlet [oneundefinedundefined, twoundefinedundefined, three] undefinedundefined=undefinedundefinednewundefinedundefinedSet([undefinedundefined1undefinedundefined,undefinedundefined2undefinedundefined,undefinedundefined3])undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That works, because internally a
destructuring assignment works by iterating over the right
value. It's kind of syntax sugar for calling
undefinedundefinedfor..of over the value to the
right of undefinedundefined= and assigning the
values.
undefinedundefined
smart header="Assign to anything at the left-side" We can use any "assignables" at the left side.
undefinedundefinedFor instance, an object property: run let user = {}; [user.name, user.surname] = "John Smith".split(''' ''');
undefinedundefinedalert(user.name); // John alert(user.surname); // Smith
undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefinedsmart header="Looping with .entries()" In the previous chapter we saw the undefinedundefinedObject.entries(obj) method.undefinedundefined
undefinedundefinedWe can use it with destructuring to loop over keys-and-values of an object:
undefinedundefinedrun let user = { name: "John", age: 30 };
undefinedundefined// loop over keys-and-values
undefinedundefined! for (let [key, value] of
Object.entries(user)) { undefinedundefined/!
alert(undefinedundefined${key}:${value}); //
name:John, then age:30 }
undefinedundefined
The similar code for
a undefinedundefinedMap is simpler, as it's
iterable:undefinedundefined
run let user = new Map(); user.set("name", "John"); user.set("age", "30");
undefinedundefinedundefinedundefined! //
Map iterates as [key, value] pairs, very convenient for
destructuring for (let [key, value] of user) {
undefinedundefined/!
alert(undefinedundefined${key}:${value}); //
name:John, then age:30 }undefinedundefined
undefinedundefinedundefinedundefined
undefinedundefinedsmart header="Swap variables trick" There's a well-known trick for swapping values of two variables using a destructuring assignment:
undefinedundefinedrun let guest = "Jane"; let admin = "Pete";
undefinedundefined// Let's swap the values: make guest=Pete, admin=Jane undefinedundefined! [guest, admin] = [admin, guest]; undefinedundefined/!undefinedundefined
undefinedundefined
alert(undefinedundefined${guest} ${admin}); // Pete
Jane (successfully swapped!)undefinedundefined
undefinedundefined
Here we create a temporary array of two variables and immediately destructure it in swapped order.
We can swap more than two variables this way.undefinedundefinedundefinedundefinedUsually, if the array is longer than the list at the left, the "extra" items are omitted.
undefinedundefinedFor example, here only two items are taken, and the rest is just ignored:
undefinedundefinedrun let [name1, name2] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
undefinedundefinedalert(name1); // Julius alert(name2); // Caesar // Further items aren't assigned anywhere
undefinedundefinedIf we'd like also to gather all that
follows - we can add one more parameter that gets "the rest"
using three dots
undefinedundefined"...":undefinedundefined
run let [name1, name2, undefinedundefined!…restundefinedundefined/!] = ["Julius", "Caesar", undefinedundefined!"Consul", "of the Roman Republic"undefinedundefined/!];undefinedundefined
undefinedundefinedundefinedundefined! // rest is array of items, starting from the 3rd one alert(rest[0]); // Consul alert(rest[1]); // of the Roman Republic alert(rest.length); // 2 undefinedundefined/! undefinedundefined
undefinedundefinedThe value of
undefinedundefinedrest is the array of the
remaining array elements.undefinedundefined
We can use any other variable name in place
of undefinedundefinedrest, just make sure it has
three dots before it and goes last in the destructuring
assignment.undefinedundefined
undefinedundefinedjs run let [name1, name2, *!*...titles*/!*] = ["Julius", "Caesar", "Consul", "of the Roman Republic"]; // now titles = ["Consul", "of the Roman Republic"]undefinedundefined
If the array is shorter than the list of variables at the left, there'll be no errors. Absent values are considered undefined:
undefinedundefinedrun undefinedundefined! let [firstName, surname] = []; undefinedundefined/!undefinedundefined
undefinedundefinedalert(firstName); // undefined alert(surname); // undefined
undefinedundefinedIf we want a "default" value to replace
the missing one, we can provide it using
undefinedundefined=:undefinedundefined
run undefinedundefined! // default values let [name = "Guest", surname = "Anonymous"] = ["Julius"]; undefinedundefined/!undefinedundefined
undefinedundefinedalert(name); // Julius (from array) alert(surname); // Anonymous (default used)
undefinedundefinedDefault values can be more complex expressions or even function calls. They are evaluated only if the value is not provided.
undefinedundefinedFor instance,
here we use the undefinedundefinedprompt function
for two defaults:undefinedundefined
run // runs only prompt for surname let [name = prompt(‘name?'''), surname = prompt(‘surname?''')] = ["Julius"];
undefinedundefinedalert(name); // Julius (from array) alert(surname); // whatever prompt gets
undefinedundefinedPlease note: the
undefinedundefinedprompt will run only for the
missing value
(undefinedundefinedsurname).undefinedundefined
The destructuring assignment also works with objects.
undefinedundefinedThe basic syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedletundefinedundefined{var1undefinedundefined, var2undefinedundefined}undefinedundefined=undefinedundefined{undefinedundefinedvar1undefinedundefined:…undefinedundefined,undefinedundefinedvar2undefinedundefined:…undefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
We should have an existing object at
the right side, that we want to split into variables. The left
side contains an object-like "pattern" for corresponding
properties. In the simplest case, that's a list of variable
names in undefinedundefined{...}.undefinedundefined
For instance:
undefinedundefinedrun let options = { title: "Menu", width: 100, height: 200 };
undefinedundefinedundefinedundefined! let {title, width, height} = options; undefinedundefined/!undefinedundefined
undefinedundefinedalert(title); // Menu alert(width); // 100 alert(height); // 200
undefinedundefinedProperties
undefinedundefinedoptions.title,
undefinedundefinedoptions.width and
undefinedundefinedoptions.height are assigned to
the corresponding variables.undefinedundefined
The order does not matter. This works too:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// changed the order in let {...}undefinedundefinedundefinedundefinedundefinedundefinedletundefinedundefined{heightundefinedundefined, widthundefinedundefined, titleundefinedundefined}undefinedundefined=undefinedundefined{undefinedundefinedtitleundefinedundefined:undefinedundefined"Menu"undefinedundefined,undefinedundefinedheightundefinedundefined:undefinedundefined200undefinedundefined,undefinedundefinedwidthundefinedundefined:undefinedundefined100undefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The pattern on the left side may be more complex and specify the mapping between properties and variables.
undefinedundefinedIf we want to assign a
property to a variable with another name, for instance, make
undefinedundefinedoptions.width go into the
variable named undefinedundefinedw, then we can set
the variable name using a colon:undefinedundefined
run let options = { title: "Menu", width: 100, height: 200 };
undefinedundefinedundefinedundefined! // { sourceProperty: targetVariable } let {width: w, height: h, title} = options; undefinedundefined/!undefinedundefined
undefinedundefined// width -> w // height -> h // title -> title
undefinedundefinedalert(title); // Menu alert(w); // 100 alert(h); // 200
undefinedundefinedThe colon shows "what : goes where". In
the example above the property
undefinedundefinedwidth goes to
undefinedundefinedw, property
undefinedundefinedheight goes to
undefinedundefinedh, and
undefinedundefinedtitle is assigned to the same
name.undefinedundefined
For potentially
missing properties we can set default values using
undefinedundefined"=", like this:undefinedundefined
run let options = { title: "Menu" };
undefinedundefinedundefinedundefined! let {width = 100, height = 200, title} = options; undefinedundefined/!undefinedundefined
undefinedundefinedalert(title); // Menu alert(width); // 100 alert(height); // 200
undefinedundefinedJust like with arrays or function parameters, default values can be any expressions or even function calls. They will be evaluated if the value is not provided.
undefinedundefinedIn the code below
undefinedundefinedprompt asks for
undefinedundefinedwidth, but not for
undefinedundefinedtitle:undefinedundefined
run let options = { title: "Menu" };
undefinedundefinedundefinedundefined! let {width = prompt("width?"), title = prompt("title?")} = options; undefinedundefined/!undefinedundefined
undefinedundefinedalert(title); // Menu alert(width); // (whatever the result of prompt is)
undefinedundefinedWe also can combine both the colon and equality:
undefinedundefinedrun let options = { title: "Menu" };
undefinedundefinedundefinedundefined! let {width: w = 100, height: h = 200, title} = options; undefinedundefined/!undefinedundefined
undefinedundefinedalert(title); // Menu alert(w); // 100 alert(h); // 200
undefinedundefinedIf we have a complex object with many properties, we can extract only what we need:
undefinedundefinedrun let options = { title: "Menu", width: 100, height: 200 };
undefinedundefined// only extract title as a variable let { title } = options;
undefinedundefinedalert(title); // Menu
undefinedundefinedWhat if the object has more properties than we have variables? Can we take some and then assign the "rest" somewhere?
undefinedundefinedWe can use the rest pattern, just like we did with arrays. It's not supported by some older browsers (IE, use Babel to polyfill it), but works in modern ones.
undefinedundefinedIt looks like this:
undefinedundefinedrun let options = { title: "Menu", height: 200, width: 100 };
undefinedundefinedundefinedundefined! // title = property named title // rest = object with the rest of properties let {title, …rest} = options; undefinedundefined/!undefinedundefined
undefinedundefined// now title="Menu", rest={height: 200, width: 100} alert(rest.height); // 200 alert(rest.width); // 100
undefinedundefined
undefinedundefinedsmart header="Gotcha if there's noletundefinedundefined" In the examples above variables were declared right in the assignment:let
{…} =
{…}undefinedundefined. Of course, we could use existing variables too, withoutlet`.
But there's a catch.undefinedundefined
This won't work: run let title, width, height;
undefinedundefined// error in this line {title, width, height} = {title: "Menu", width: 200, height: 100};
undefinedundefinedThe problem is that JavaScript treats
undefinedundefined{...} in the main code flow (not
inside another expression) as a code block. Such code blocks can
be used to group statements, like this:undefinedundefined
undefinedundefinedjs run { // a code block let message = "Hello"; // ... alert( message ); }undefinedundefined
So here JavaScript assumes that we have a code block, that's why there's an error. We want destructuring instead.
undefinedundefinedTo show JavaScript that it's
not a code block, we can wrap the expression in parentheses
undefinedundefined(...):undefinedundefined
run let title, width, height;
undefinedundefined// okay now undefinedundefined!(undefinedundefined/!{title, width, height} = {title: "Menu", width: 200, height: 100}undefinedundefined!)undefinedundefined/!;undefinedundefined
undefinedundefinedalert( title ); // Menu
undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefinedIf an object or an array contain other nested objects and arrays, we can use more complex left-side patterns to extract deeper portions.
undefinedundefinedIn the code below
undefinedundefinedoptions has another object in the
property undefinedundefinedsize and an array in the
property undefinedundefineditems. The pattern at
the left side of the assignment has the same structure to
extract values from them:undefinedundefined
run let options = { size: { width: 100,
height: 200 }, items: ["Cake", "Donut"], extra:
trueundefinedundefined
};undefinedundefined
// destructuring assignment split in multiple lines for clarity let { size: { // put size here width, height }, items: [item1, item2], // assign items here title = "Menu" // not present in the object (default value is used) } = options;
undefinedundefinedalert(title); // Menu alert(width); // 100 alert(height); // 200 alert(item1); // Cake alert(item2); // Donut
undefinedundefinedAll properties of
undefinedundefinedoptions object except
undefinedundefinedextra that is absent in the left
part, are assigned to corresponding variables:undefinedundefined
undefinedundefinedundefinedundefined
Finally, we have
undefinedundefinedwidth,
undefinedundefinedheight,
undefinedundefineditem1,
undefinedundefineditem2 and
undefinedundefinedtitle from the default
value.undefinedundefined
Note that there
are no variables for undefinedundefinedsize and
undefinedundefineditems, as we take their content
instead.undefinedundefined
There are times when a function has many parameters, most of which are optional. That's especially true for user interfaces. Imagine a function that creates a menu. It may have a width, a height, a title, items list and so on.
undefinedundefinedHere's a bad way to write such function:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedshowMenu(title undefinedundefined=undefinedundefined"Untitled"undefinedundefined, width undefinedundefined=undefinedundefined200undefinedundefined, height undefinedundefined=undefinedundefined100undefinedundefined, items undefinedundefined= []) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In real-life, the problem is how to remember the order of arguments. Usually IDEs try to help us, especially if the code is well-documented, but still… Another problem is how to call a function when most parameters are ok by default.
undefinedundefinedLike this?
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// undefined where default values are fineundefinedundefinedundefinedundefinedundefinedundefinedshowMenu(undefinedundefined"My Menu"undefinedundefined,undefinedundefinedundefinedundefinedundefined,undefinedundefinedundefinedundefinedundefined, [undefinedundefined"Item1"undefinedundefined,undefinedundefined"Item2"])undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That's ugly. And becomes unreadable when we deal with more parameters.
undefinedundefinedDestructuring comes to the rescue!
undefinedundefinedWe can pass parameters as an object, and the function immediately destructurizes them into variables:
undefinedundefinedrun // we pass object to function let options = { title: "My menu", items: ["Item1", "Item2"] };
undefinedundefined// …and it
immediately expands it to variables function
showMenu(undefinedundefined!{title = "Untitled", width
= 200, height = 100, items = []}undefinedundefined/!) {
// title, items - taken from options, // width, height -
defaults used alert(
undefinedundefined${title} ${width} ${height} ); //
My Menu 200 100 alert( items ); // Item1, Item2
}undefinedundefined
showMenu(options);
undefinedundefinedWe can also use more complex destructuring with nested objects and colon mappings:
undefinedundefinedrun let options = { title: "My menu", items: ["Item1", "Item2"] };
undefinedundefined
undefinedundefined! function showMenu({ title =
"Untitled", width: w = 100, // width goes to w height: h = 200,
// height goes to h items: [item1, item2] // items first element
goes to item1, second to item2 }) {
undefinedundefined/! alert(
undefinedundefined${title} ${w} ${h} ); // My Menu
100 200 alert( item1 ); // Item1 alert( item2 ); // Item2
}undefinedundefined
showMenu(options);
undefinedundefinedThe full syntax is the same as for a destructuring assignment:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunction(undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedincomingPropertyundefinedundefined: varName undefinedundefined= defaultValueundefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined})undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Then, for an object of parameters,
there will be a variable undefinedundefinedvarName
for property undefinedundefinedincomingProperty,
with undefinedundefineddefaultValue by
default.undefinedundefined
Please note
that such destructuring assumes that
undefinedundefinedshowMenu() does have an argument.
If we want all values by default, then we should specify an
empty object:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedshowMenu(undefinedundefined{})undefinedundefined;undefinedundefined// ok, all values are defaultundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedshowMenu()undefinedundefined;undefinedundefined// this would give an errorundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
We can fix this by making
undefinedundefined{} the default value for the
whole object of parameters:undefinedundefined
``undefinedundefinedjs run function showMenu({ title = "Menu", width = 100, height = 200 }*!* = {}*/!*) { alert(${title}
${width} ${height}` ); }undefinedundefined
showMenu(); // Menu 100 200
undefinedundefinedIn the code above, the whole arguments
object is undefinedundefined{} by default, so
there's always something to destructurize.undefinedundefined
The full object
syntax:
undefinedundefinedjs let {prop : varName = default, ...rest} = objectundefinedundefined
This means that property
undefinedundefinedprop should go into the
variable undefinedundefinedvarName and, if no
such property exists, then the
undefinedundefineddefault value should be
used.undefinedundefined
Object
properties that have no mapping are copied to the
undefinedundefinedrest
object.undefinedundefined
The full array syntax:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet [item1 undefinedundefined=undefinedundefineddefaultundefinedundefined, item2undefinedundefined, ...undefinedundefinedrest] undefinedundefined= arrayundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The first item goes to
undefinedundefineditem1; the second goes into
undefinedundefineditem2, all the rest makes the
array undefinedundefinedrest.undefinedundefined
It's possible to extract data from nested arrays/objects, for that the left side must have the same structure as the right one.
undefinedundefinedLet's meet a new built-in object: undefinedundefinedDate. It stores the date, time and provides methods for date/time management.undefinedundefined
undefinedundefinedFor instance, we can use it to store creation/modification times, to measure time, or just to print out the current date.
undefinedundefinedTo create a new
undefinedundefinedDate object call
undefinedundefinednew Date() with one of the
following arguments:undefinedundefined
new Date()undefinedundefined
Without
arguments - create a undefinedundefinedDate
object for the current date and time:undefinedundefined
undefinedundefinedjs run let now = new Date(); alert( now ); // shows current date/timeundefinedundefined
new Date(milliseconds)undefinedundefined
Create a
undefinedundefinedDate object with the time
equal to number of milliseconds (1/1000 of a second) passed
after the Jan 1st of 1970 UTC+0.undefinedundefined
run // 0 means 01.01.1970 UTC+0 let Jan01_1970 = new Date(0); alert( Jan01_1970 );
undefinedundefined// now add 24 hours, get 02.01.1970 UTC+0 let Jan02_1970 = new Date(24 * 3600 * 1000); alert( Jan02_1970 );
undefinedundefinedAn integer number representing the number of milliseconds that has passed since the beginning of 1970 is called a undefinedundefinedtimestamp.undefinedundefined
undefinedundefinedIt's a lightweight numeric representation
of a date. We can always create a date from a timestamp
using undefinedundefinednew Date(timestamp) and
convert the existing undefinedundefinedDate
object to a timestamp using the
undefinedundefineddate.getTime() method (see
below).undefinedundefined
Dates
before 01.01.1970 have negative timestamps, e.g.:
undefinedundefinedjs run // 31 Dec 1969 let Dec31_1969 = new Date(-24 * 3600 * 1000); alert( Dec31_1969 );undefinedundefined
new Date(datestring)undefinedundefined
If there is a
single argument, and it's a string, then it is parsed
automatically. The algorithm is the same as
undefinedundefinedDate.parse uses, we'll cover
it later.undefinedundefined
undefinedundefinedjs run let date = new Date("2017-01-26"); alert(date); // The time is not set, so it's assumed to be midnight GMT and // is adjusted according to the timezone the code is run in // So the result could be // Thu Jan 26 2017 11:00:00 GMT+1100 (Australian Eastern Daylight Time) // or // Wed Jan 25 2017 16:00:00 GMT-0800 (Pacific Standard Time)undefinedundefined
new Date(year, month, date, hours, minutes, seconds, ms)undefinedundefined
Create the date with the given components in the local time zone. Only the first two arguments are obligatory.
undefinedundefinedyear must have 4 digits:
undefinedundefined2013 is okay,
undefinedundefined98 is
not.undefinedundefinedmonth count starts with
undefinedundefined0 (Jan), up to
undefinedundefined11 (Dec).undefinedundefined
date parameter is actually
the day of month, if absent then
undefinedundefined1 is
assumed.undefinedundefinedhours/minutes/seconds/ms is
absent, they are assumed to be equal
undefinedundefined0.undefinedundefinedFor instance:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinednewundefinedundefinedDate(undefinedundefined2011undefinedundefined,undefinedundefined0undefinedundefined,undefinedundefined1undefinedundefined,undefinedundefined0undefinedundefined,undefinedundefined0undefinedundefined,undefinedundefined0undefinedundefined,undefinedundefined0)undefinedundefined;undefinedundefined// 1 Jan 2011, 00:00:00undefinedundefinedundefinedundefinedundefinedundefinednewundefinedundefinedDate(undefinedundefined2011undefinedundefined,undefinedundefined0undefinedundefined,undefinedundefined1)undefinedundefined;undefinedundefined// the same, hours etc are 0 by defaultundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The maximal precision is 1 ms (1/1000 sec):
undefinedundefined
undefinedundefinedjs run let date = new Date(2011, 0, 1, 2, 3, 4, 567); alert( date ); // 1.01.2011, 02:03:04.567undefinedundefined
There are methods to access
the year, month and so on from the
undefinedundefinedDate object:undefinedundefined
undefinedundefinedwarn header="Not `getYear()`, but `getFullYear()`" Many JavaScript engines implement a non-standard method `getYear()`. This method is deprecated. It returns 2-digit year sometimes. Please never use it. There is `getFullYear()` for the year.undefinedundefined
Additionally, we can get a day of week:
undefinedundefined0 (Sunday) to
undefinedundefined6 (Saturday). The first day is
always Sunday, in some countries that's not so, but can't be
changed.
undefinedundefinedundefinedundefinedAll the methods above return the components relative to the local time zone.undefinedundefined
undefinedundefinedThere
are also their UTC-counterparts, that return day, month, year
and so on for the time zone UTC+0: undefinedundefinedgetUTCFullYear(),
undefinedundefinedgetUTCMonth(),
undefinedundefinedgetUTCDay(). Just insert the
undefinedundefined"UTC" right after
undefinedundefined"get".undefinedundefined
If your local time zone is shifted relative to UTC, then the code below shows different hours:
undefinedundefinedrun // current date let date = new Date();
undefinedundefined// the hour in your current time zone alert( date.getHours() );
undefinedundefined// the hour in UTC+0 time zone (London time without daylight savings) alert( date.getUTCHours() );
undefinedundefinedBesides the given methods, there are two special ones that do not have a UTC-variant:
undefinedundefinedReturns the difference between UTC and the local time zone, in minutes:
undefinedundefinedrun // if you are in timezone UTC-1, outputs 60 // if you are in timezone UTC+3, outputs -180 alert( new Date().getTimezoneOffset() );
undefinedundefinedundefinedundefined
The following methods allow to set date/time components:
undefinedundefinedsetFullYear(year, [month], [date])undefinedundefinedundefinedundefined
setMonth(month, [date])undefinedundefinedundefinedundefined
setDate(date)undefinedundefinedundefinedundefined
setHours(hour, [min], [sec], [ms])undefinedundefinedundefinedundefined
setMinutes(min, [sec], [ms])undefinedundefinedundefinedundefined
setSeconds(sec, [ms])undefinedundefinedundefinedundefined
setMilliseconds(ms)undefinedundefinedundefinedundefined
setTime(milliseconds)undefinedundefined
(sets the whole date by milliseconds since 01.01.1970
UTC)undefinedundefinedEvery one of them except
undefinedundefinedsetTime() has a UTC-variant, for
instance:
undefinedundefinedsetUTCHours().undefinedundefined
As we can see, some methods can set
multiple components at once, for example
undefinedundefinedsetHours. The components that are
not mentioned are not modified.undefinedundefined
For instance:
undefinedundefinedrun let today = new Date();
undefinedundefinedtoday.setHours(0); alert(today); // still today, but the hour is changed to 0
undefinedundefinedtoday.setHours(0, 0, 0, 0); alert(today); // still today, now 00:00:00 sharp.
undefinedundefinedThe undefinedundefinedautocorrection
is a very handy feature of undefinedundefinedDate
objects. We can set out-of-range values, and it will auto-adjust
itself.undefinedundefined
For instance:
undefinedundefined
undefinedundefinedjs run let date = new Date(2013, 0, *!*32*/!*); // 32 Jan 2013 ?!? alert(date); // ...is 1st Feb 2013!undefinedundefined
Out-of-range date components are distributed automatically.
undefinedundefinedLet's say we
need to increase the date "28 Feb 2016" by 2 days. It may be "2
Mar" or "1 Mar" in case of a leap-year. We don't need to think
about it. Just add 2 days. The
undefinedundefinedDate object will do the
rest:undefinedundefined
run let date = new Date(2016, 1, 28); undefinedundefined! date.setDate(date.getDate() + 2); undefinedundefined/!undefinedundefined
undefinedundefinedalert( date ); // 1 Mar 2016
undefinedundefinedThat feature is often used to get the date after the given period of time. For instance, let's get the date for "70 seconds after now":
undefinedundefinedrun let date = new Date(); date.setSeconds(date.getSeconds() + 70);
undefinedundefinedalert( date ); // shows the correct date
undefinedundefinedWe can also set zero or even negative values. For example:
undefinedundefinedrun let date = new Date(2016, 0, 2); // 2 Jan 2016
undefinedundefineddate.setDate(1); // set day 1 of month alert( date );
undefinedundefineddate.setDate(0); // min day is 1, so the last day of the previous month is assumed alert( date ); // 31 Dec 2015
undefinedundefinedWhen a
undefinedundefinedDate object is converted to
number, it becomes the timestamp same as
undefinedundefineddate.getTime():undefinedundefined
undefinedundefinedjs run let date = new Date(); alert(+date); // the number of milliseconds, same as date.getTime()undefinedundefined
The important side effect: dates can be subtracted, the result is their difference in ms.
undefinedundefinedThat can be used for time measurements:
undefinedundefinedrun let start = new Date(); // start measuring time
undefinedundefined// do the job for (let i = 0; i < 100000; i++) { let doSomething = i * i * i; }
undefinedundefinedlet end = new Date(); // end measuring time
undefinedundefinedalert(
undefinedundefinedThe loop took ${end - start} ms
);
undefinedundefined
If we only want to measure
time, we don't need the undefinedundefinedDate
object.undefinedundefined
There's a
special method undefinedundefinedDate.now() that
returns the current timestamp.undefinedundefined
It is semantically equivalent to
undefinedundefinednew Date().getTime(), but it
doesn't create an intermediate
undefinedundefinedDate object. So it's faster and
doesn't put pressure on garbage collection.undefinedundefined
It is used mostly for convenience or when performance matters, like in games in JavaScript or other specialized applications.
undefinedundefinedSo this is probably better:
undefinedundefinedrun undefinedundefined! let start = Date.now(); // milliseconds count from 1 Jan 1970 undefinedundefined/!undefinedundefined
undefinedundefined// do the job for (let i = 0; i < 100000; i++) { let doSomething = i * i * i; }
undefinedundefinedundefinedundefined! let end = Date.now(); // done undefinedundefined/!undefinedundefined
undefinedundefinedalert(
undefinedundefinedThe loop took ${end - start} ms
); // subtract numbers, not dates
undefinedundefined
If we want a reliable benchmark of CPU-hungry function, we should be careful.
undefinedundefinedFor instance, let's measure two functions that calculate the difference between two dates: which one is faster?
undefinedundefinedSuch performance measurements are often called "benchmarks".
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// we have date1 and date2, which function faster returns their difference in ms?undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefineddiffSubtract(date1undefinedundefined, date2) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturn date2 undefinedundefined- date1undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// orundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefineddiffGetTime(date1undefinedundefined, date2) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefineddate2.undefinedundefinedgetTime() undefinedundefined-undefinedundefineddate1.undefinedundefinedgetTime()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
These two do exactly the same thing,
but one of them uses an explicit
undefinedundefineddate.getTime() to get the date in
ms, and the other one relies on a date-to-number transform.
Their result is always the same.undefinedundefined
So, which one is faster?
undefinedundefinedThe first idea may be to run them many times in a row and measure the time difference. For our case, functions are very simple, so we have to do it at least 100000 times.
undefinedundefinedLet's measure:
undefinedundefinedrun function diffSubtract(date1, date2) { return date2 - date1; }
undefinedundefinedfunction diffGetTime(date1, date2) { return date2.getTime() - date1.getTime(); }
undefinedundefinedfunction bench(f) { let date1 = new Date(0); let date2 = new Date();
undefinedundefinedlet start = Date.now(); for (let i = 0; i < 100000; i++) f(date1, date2); return Date.now() - start; }
undefinedundefinedalert( ‘Time of diffSubtract:''' + bench(diffSubtract) + ‘ms' ); alert( ‘Time of diffGetTime:''' + bench(diffGetTime) + ‘ms' );
undefinedundefinedWow! Using
undefinedundefinedgetTime() is so much faster!
That's because there's no type conversion, it is much easier for
engines to optimize.undefinedundefined
Okay, we have something. But that's not a good benchmark yet.
undefinedundefinedImagine that at the time of running
undefinedundefinedbench(diffSubtract) CPU was doing
something in parallel, and it was taking resources. And by the
time of running
undefinedundefinedbench(diffGetTime) that work has
finished.undefinedundefined
A pretty real scenario for a modern multi-process OS.
undefinedundefinedAs a result, the first benchmark will have less CPU resources than the second. That may lead to wrong results.
undefinedundefinedundefinedundefinedFor more reliable benchmarking, the whole pack of benchmarks should be rerun multiple times.undefinedundefined
undefinedundefinedFor example, like this:
undefinedundefinedrun function diffSubtract(date1, date2) { return date2 - date1; }
undefinedundefinedfunction diffGetTime(date1, date2) { return date2.getTime() - date1.getTime(); }
undefinedundefinedfunction bench(f) { let date1 = new Date(0); let date2 = new Date();
undefinedundefinedlet start = Date.now(); for (let i = 0; i < 100000; i++) f(date1, date2); return Date.now() - start; }
undefinedundefinedlet time1 = 0; let time2 = 0;
undefinedundefinedundefinedundefined! // run bench(diffSubtract) and bench(diffGetTime) each 10 times alternating for (let i = 0; i < 10; i++) { time1 += bench(diffSubtract); time2 += bench(diffGetTime); } undefinedundefined/!undefinedundefined
undefinedundefinedalert( ‘Total time for diffSubtract:''' + time1 ); alert( ‘Total time for diffGetTime:''' + time2 );
undefinedundefinedModern JavaScript engines start applying advanced optimizations only to "hot code" that executes many times (no need to optimize rarely executed things). So, in the example above, first executions are not well-optimized. We may want to add a heat-up run:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// added for "heating up" prior to the main loopundefinedundefinedundefinedundefinedundefinedundefinedbench(diffSubtract)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedbench(diffGetTime)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// now benchmarkundefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedlet i undefinedundefined=undefinedundefined0undefinedundefined; i undefinedundefined<undefinedundefined10undefinedundefined; iundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefined time1 undefinedundefined+=undefinedundefinedbench(diffSubtract)undefinedundefined;undefinedundefinedundefinedundefined time2 undefinedundefined+=undefinedundefinedbench(diffGetTime)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
warn header="Be careful doing microbenchmarking" Modern JavaScript engines perform many optimizations. They may tweak results of "artificial tests" compared to "normal usage", especially when we benchmark something very small, such as how an operator works, or a built-in function. So if you seriously want to understand performance, then please study how the JavaScript engine works. And then you probably won't need microbenchmarks at all.
undefinedundefinedThe great pack of articles about V8 can be found at undefinedundefinedhttp://mrale.ph. undefinedundefined
undefinedundefinedThe method undefinedundefinedDate.parse(str) can read a date from a string.undefinedundefined
undefinedundefinedThe
string format should be:
undefinedundefinedYYYY-MM-DDTHH:mm:ss.sssZ,
where:undefinedundefined
YYYY-MM-DD
- is the date: year-month-day.undefinedundefined"T" is used as the
delimiter.undefinedundefinedHH:mm:ss.sss - is the time:
hours, minutes, seconds and milliseconds.undefinedundefined
'Z' part denotes the time zone
in the format undefinedundefined+-hh:mm. A single
letter undefinedundefinedZ would mean
UTC+0.undefinedundefinedShorter variants are also possible, like
undefinedundefinedYYYY-MM-DD or
undefinedundefinedYYYY-MM or even
undefinedundefinedYYYY.undefinedundefined
The call to
undefinedundefinedDate.parse(str) parses the string
in the given format and returns the timestamp (number of
milliseconds from 1 Jan 1970 UTC+0). If the format is invalid,
returns undefinedundefinedNaN.undefinedundefined
For instance:
undefinedundefinedrun let ms = Date.parse(‘2012-01-26T13:51:50.417-07:00');
undefinedundefinedalert(ms); // 1327611110417 (timestamp)
undefinedundefinedWe can instantly create a
undefinedundefinednew Date object from the
timestamp:undefinedundefined
run let date = new Date( Date.parse(‘2012-01-26T13:51:50.417-07:00') );
undefinedundefinedalert(date);undefinedundefined
undefinedundefined
Date
objects always carry both.undefinedundefinedgetDay() are also counted from
zero (that's Sunday).undefinedundefinedDate auto-corrects itself
when out-of-range components are set. Good for
adding/subtracting days/months/hours.undefinedundefinedDate becomes the timestamp when
converted to a number.undefinedundefinedDate.now() to get the current
timestamp fast.undefinedundefinedNote that unlike many other systems, timestamps in JavaScript are in milliseconds, not in seconds.
undefinedundefinedSometimes we need more precise time measurements. JavaScript itself does not have a way to measure time in microseconds (1 millionth of a second), but most environments provide it. For instance, browser has undefinedundefinedperformance.now() that gives the number of milliseconds from the start of page loading with microsecond precision (3 digits after the point):undefinedundefined
undefinedundefined
undefinedundefinedjs run alert(`Loading started ${performance.now()}ms ago`); // Something like: "Loading started 34731.26000000001ms ago" // .26 is microseconds (260 microseconds) // more than 3 digits after the decimal point are precision errors, only the first 3 are correctundefinedundefined
Node.js has
undefinedundefinedmicrotime module and other ways.
Technically, almost any device and environment allows to get
more precision, it's just not in
undefinedundefinedDate.undefinedundefined
Let's say we have a complex object, and we'd like to convert it into a string, to send it over a network, or just to output it for logging purposes.
undefinedundefinedNaturally, such a string should include all important properties.
undefinedundefinedWe could implement the conversion like this:
undefinedundefinedrun let user = { name: "John", age: 30,
undefinedundefined
undefinedundefined! toString() { return
undefinedundefined{name: "${this.name}", age: ${this.age}};
} undefinedundefined/! };undefinedundefined
alert(user); // {name: "John", age: 30}
undefinedundefined…But in the process of development, new
properties are added, old properties are renamed and removed.
Updating such undefinedundefinedtoString every time
can become a pain. We could try to loop over properties in it,
but what if the object is complex and has nested objects in
properties? We'd need to implement their conversion as
well.undefinedundefined
Luckily, there's no need to write the code to handle all this. The task has been solved already.
undefinedundefinedThe undefinedundefinedJSON (JavaScript Object Notation) is a general format to represent values and objects. It is described as in undefinedundefinedRFC 4627 standard. Initially it was made for JavaScript, but many other languages have libraries to handle it as well. So it's easy to use JSON for data exchange when the client uses JavaScript and the server is written on Ruby/PHP/Java/Whatever.undefinedundefined
undefinedundefinedJavaScript provides methods:
undefinedundefinedJSON.stringify to convert
objects into JSON.undefinedundefinedJSON.parse to convert JSON
back into an object.undefinedundefinedFor instance, here we
undefinedundefinedJSON.stringify a student: run let
student = { name: ‘John', age: 30, isAdmin: false, courses:
[‘html', ‘css', ‘js'], wife: null };undefinedundefined
undefinedundefined! let json = JSON.stringify(student); undefinedundefined/!undefinedundefined
undefinedundefinedalert(typeof json); // we've got a string!
undefinedundefinedalert(json); undefinedundefined! /* JSON-encoded object: { "name": "John", "age": 30, "isAdmin": false, "courses": ["html", "css", "js"], "wife": null } undefinedundefined/ /!* undefinedundefined
undefinedundefinedThe method
undefinedundefinedJSON.stringify(student) takes the
object and converts it into a string.undefinedundefined
The resulting
undefinedundefinedjson string is called a
undefinedundefinedJSON-encoded or
undefinedundefinedserialized or
undefinedundefinedstringified or
undefinedundefinedmarshalled object. We are ready to
send it over the wire or put into a plain data
store.undefinedundefined
Please note that a JSON-encoded object has several important differences from the object literal:
undefinedundefined'John' becomes
undefinedundefined"John".undefinedundefinedage:30 becomes
undefinedundefined"age":30.undefinedundefined
undefinedundefinedJSON.stringify can be applied to
primitives as well.undefinedundefined
JSON supports following data types:
undefinedundefined{ ... }undefinedundefined[ ... ]undefinedundefinedtrue/false,undefinedundefined
null.undefinedundefined
For instance:
undefinedundefinedrun // a number in JSON is just a number alert( JSON.stringify(1) ) // 1
undefinedundefined// a string in JSON is still a string, but double-quoted alert( JSON.stringify(‘test') ) // "test"
undefinedundefinedalert( JSON.stringify(true) ); // true
undefinedundefinedalert( JSON.stringify([1, 2, 3]) ); // [1,2,3]
undefinedundefinedJSON is data-only language-independent
specification, so some JavaScript-specific object properties are
skipped by
undefinedundefinedJSON.stringify.undefinedundefined
Namely:
undefinedundefinedundefined.undefinedundefined
run let user = { sayHi() { // ignored alert("Hello"); }, [Symbol("id")]: 123, // ignored something: undefined // ignored };
undefinedundefinedalert( JSON.stringify(user) ); // {} (empty object)
undefinedundefinedUsually that's fine. If that's not what we want, then soon we'll see how to customize the process.
undefinedundefinedThe great thing is that nested objects are supported and converted automatically.
undefinedundefinedFor instance:
undefinedundefinedrun let meetup = { title: "Conference", undefinedundefined! room: { number: 23, participants: ["john", "ann"] } undefinedundefined/! };undefinedundefined
undefinedundefinedalert( JSON.stringify(meetup) ); /* The whole structure is stringified: { "title":"Conference", "room":{"number":23,"participants":["john","ann"]}, } */
undefinedundefinedThe important limitation: there must be no circular references.
undefinedundefinedFor instance:
undefinedundefinedrun let room = { number: 23 };
undefinedundefinedlet meetup = { title: "Conference", participants: ["john", "ann"] };
undefinedundefinedmeetup.place = room; // meetup references room room.occupiedBy = meetup; // room references meetup
undefinedundefinedundefinedundefined! JSON.stringify(meetup); // Error: Converting circular structure to JSON undefinedundefined/! undefinedundefined
undefinedundefinedHere, the conversion
fails, because of circular reference:
undefinedundefinedroom.occupiedBy references
undefinedundefinedmeetup, and
undefinedundefinedmeetup.place references
undefinedundefinedroom:undefinedundefined
undefinedundefinedundefinedundefined
The full syntax of undefinedundefinedJSON.stringify
is:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet json undefinedundefined=undefinedundefinedJSON.undefinedundefinedstringify(value[undefinedundefined, replacerundefinedundefined, space])undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
function(key, value).
undefinedundefinedMost of the
time, undefinedundefinedJSON.stringify is used with
the first argument only. But if we need to fine-tune the
replacement process, like to filter out circular references, we
can use the second argument of
undefinedundefinedJSON.stringify.undefinedundefined
If we pass an array of properties to it, only these properties will be encoded.
undefinedundefinedFor instance:
undefinedundefinedrun let room = { number: 23 };
undefinedundefinedlet meetup = { title: "Conference", participants: [{name: "John"}, {name: "Alice"}], place: room // meetup references room };
undefinedundefinedroom.occupiedBy = meetup; // room references meetup
undefinedundefinedalert( JSON.stringify(meetup, undefinedundefined![‘title', ‘participants']undefinedundefined/!) ); // {"title":"Conference","participants":[{},{}]} undefinedundefined
undefinedundefinedHere we are probably
too strict. The property list is applied to the whole object
structure. So the objects in
undefinedundefinedparticipants are empty, because
undefinedundefinedname is not in the
list.undefinedundefined
Let's include in
the list every property except
undefinedundefinedroom.occupiedBy that would cause
the circular reference:undefinedundefined
run let room = { number: 23 };
undefinedundefinedlet meetup = { title: "Conference", participants: [{name: "John"}, {name: "Alice"}], place: room // meetup references room };
undefinedundefinedroom.occupiedBy = meetup; // room references meetup
undefinedundefinedalert( JSON.stringify(meetup, undefinedundefined![‘title', ‘participants', ‘place', ‘name', ‘number']undefinedundefined/!) ); /undefinedundefined { "title":"Conference", "participants":[{"name":"John"},{"name":"Alice"}], "place":{"number":23} } / undefinedundefined
undefinedundefinedNow everything except
undefinedundefinedoccupiedBy is serialized. But the
list of properties is quite long.undefinedundefined
Fortunately, we can use a function instead of
an array as the
undefinedundefinedreplacer.undefinedundefined
The function will be called for every
undefinedundefined(key, value) pair and should
return the "replaced" value, which will be used instead of the
original one. Or undefinedundefinedundefined if the
value is to be skipped.undefinedundefined
In our case, we can return undefinedundefinedvalue
"as is" for everything except
undefinedundefinedoccupiedBy. To ignore
undefinedundefinedoccupiedBy, the code below
returns
undefinedundefinedundefined:undefinedundefined
run let room = { number: 23 };
undefinedundefinedlet meetup = { title: "Conference", participants: [{name: "John"}, {name: "Alice"}], place: room // meetup references room };
undefinedundefinedroom.occupiedBy = meetup; // room references meetup
undefinedundefinedalert( JSON.stringify(meetup, function
replacer(key, value) {
alert(undefinedundefined${key}: ${value}); return
(key == ‘occupiedBy') ? undefined : value;
}));undefinedundefined
undefinedundefined [object Object]undefinedundefined
title: Conference participants: [object Object],[object
Object] 0: [object Object] name: John 1: [object Object] name:
Alice place: [object Object] number: 23 occupiedBy: [object
Object] */
undefinedundefined
Please note
that undefinedundefinedreplacer function gets every
key/value pair including nested objects and array items. It is
applied recursively. The value of
undefinedundefinedthis inside
undefinedundefinedreplacer is the object that
contains the current property.undefinedundefined
The first call is special. It is made using a
special "wrapper object":
undefinedundefined{"": meetup}. In other words, the
first undefinedundefined(key, value) pair has an
empty key, and the value is the target object as a whole. That's
why the first line is
undefinedundefined":[object Object]" in the example
above.undefinedundefined
The idea is to
provide as much power for
undefinedundefinedreplacer as possible: it has a
chance to analyze and replace/skip even the whole object if
necessary.undefinedundefined
The third argument of
undefinedundefinedJSON.stringify(value, replacer, space)
is the number of spaces to use for pretty
formatting.undefinedundefined
Previously, all stringified objects had no indents and extra
spaces. That's fine if we want to send an object over a network.
The undefinedundefinedspace argument is used
exclusively for a nice output.undefinedundefined
Here undefinedundefinedspace = 2
tells JavaScript to show nested objects on multiple lines, with
indentation of 2 spaces inside an object:undefinedundefined
run let user = { name: "John", age: 25, roles: { isAdmin: false, isEditor: true } };
undefinedundefinedalert(JSON.stringify(user, null, 2)); /* two-space indents: { "name": "John", "age": 25, "roles": { "isAdmin": false, "isEditor": true } } */
undefinedundefined/* for JSON.stringify(user, null, 4) the result would be more indented: { "name": "John", "age": 25, "roles": { "isAdmin": false, "isEditor": true } } */
undefinedundefinedThe third argument can also be a string. In this case, the string is used for indentation instead of a number of spaces.
undefinedundefinedThe
undefinedundefinedspace parameter is used solely
for logging and nice-output purposes.undefinedundefined
Like undefinedundefinedtoString
for string conversion, an object may provide method
undefinedundefinedtoJSON for to-JSON conversion.
undefinedundefinedJSON.stringify automatically
calls it if available.undefinedundefined
For instance:
undefinedundefinedrun let room = { number: 23 };
undefinedundefinedlet meetup = { title: "Conference", date: new Date(Date.UTC(2017, 0, 1)), room };
undefinedundefinedalert( JSON.stringify(meetup) ); /undefinedundefined { "title":"Conference", !undefinedundefined "date":"2017-01-01T00:00:00.000Z", // (1) /!undefinedundefined "room": {"number":23} // (2) } / undefinedundefined
undefinedundefinedHere we can see that
undefinedundefineddateundefinedundefined(1)
became a string. That's because all dates have a built-in
undefinedundefinedtoJSON method which returns such
kind of string.undefinedundefined
Now
let's add a custom undefinedundefinedtoJSON for our
object
undefinedundefinedroomundefinedundefined(2):undefinedundefined
run let room = { number: 23, undefinedundefined! toJSON() { return this.number; } undefinedundefined/! };undefinedundefined
undefinedundefinedlet meetup = { title: "Conference", room };
undefinedundefinedundefinedundefined! alert( JSON.stringify(room) ); // 23 undefinedundefined/!undefinedundefined
undefinedundefinedalert( JSON.stringify(meetup) ); /undefinedundefined { "title":"Conference", !undefinedundefined "room": 23 /!undefinedundefined } / undefinedundefined
undefinedundefinedAs we can see,
undefinedundefinedtoJSON is used both for the
direct call undefinedundefinedJSON.stringify(room)
and when undefinedundefinedroom is nested in
another encoded object.undefinedundefined
To decode a JSON-string, we need another method named undefinedundefinedJSON.parse.undefinedundefined
undefinedundefinedThe syntax:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet value undefinedundefined=undefinedundefinedJSON.undefinedundefinedparse(strundefinedundefined, [reviver])undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
(key, value) pair and can
transform the value.
undefinedundefinedFor instance:
undefinedundefinedrun // stringified array let numbers = "[0, 1, 2, 3]";
undefinedundefinednumbers = JSON.parse(numbers);
undefinedundefinedalert( numbers[1] ); // 1
undefinedundefinedOr for nested objects:
undefinedundefinedrun let userData = ‘{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }''';
undefinedundefinedlet user = JSON.parse(userData);
undefinedundefinedalert( user.friends[1] ); // 1
undefinedundefinedThe JSON may be as complex as necessary, objects and arrays can include other objects and arrays. But they must obey the same JSON format.
undefinedundefinedHere are typical mistakes in hand-written JSON (sometimes we have to write it for debugging purposes):
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet json undefinedundefined=undefinedundefined`{undefinedundefinedundefinedundefinedundefinedundefined *!*name*/!*: "John", // mistake: property name without quotesundefinedundefinedundefinedundefinedundefinedundefined "surname": *!*'Smith'*/!*, // mistake: single quotes in value (must be double)undefinedundefinedundefinedundefinedundefinedundefined *!*'isAdmin'*/!*: false // mistake: single quotes in key (must be double)undefinedundefinedundefinedundefinedundefinedundefined "birthday": *!*new Date(2000, 2, 3)*/!*, // mistake: no "new" is allowed, only bare valuesundefinedundefinedundefinedundefinedundefinedundefined "friends": [0,1,2,3] // here all fineundefinedundefinedundefinedundefinedundefinedundefined}`undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Besides, JSON does not support comments. Adding a comment to JSON makes it invalid.
undefinedundefinedThere's another format named undefinedundefinedJSON5, which allows unquoted keys, comments etc. But this is a standalone library, not in the specification of the language.undefinedundefined
undefinedundefinedThe regular JSON is that strict not because its developers are lazy, but to allow easy, reliable and very fast implementations of the parsing algorithm.
undefinedundefinedImagine, we got a
stringified undefinedundefinedmeetup object from
the server.undefinedundefined
It looks like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// title: (meetup title), date: (meetup date)undefinedundefinedundefinedundefinedundefinedundefinedlet str undefinedundefined=undefinedundefined'{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…And now we need to undefinedundefineddeserialize it, to turn back into JavaScript object.undefinedundefined
undefinedundefined
Let's do it by calling
undefinedundefinedJSON.parse:undefinedundefined
run let str = ‘{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}''';
undefinedundefinedlet meetup = JSON.parse(str);
undefinedundefinedundefinedundefined! alert( meetup.date.getDate() ); // Error! undefinedundefined/! undefinedundefined
undefinedundefinedWhoops! An error!
undefinedundefinedThe value of
undefinedundefinedmeetup.date is a string, not a
undefinedundefinedDate object. How could
undefinedundefinedJSON.parse know that it should
transform that string into a
undefinedundefinedDate?undefinedundefined
Let's pass to
undefinedundefinedJSON.parse the reviving function
as the second argument, that returns all values "as is", but
undefinedundefineddate will become a
undefinedundefinedDate:undefinedundefined
run let str = ‘{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}''';
undefinedundefinedundefinedundefined! let meetup = JSON.parse(str, function(key, value) { if (key == ‘date') return new Date(value); return value; }); undefinedundefined/!undefinedundefined
undefinedundefinedalert( meetup.date.getDate() ); // now works!
undefinedundefinedBy the way, that works for nested objects as well:
undefinedundefined
``undefinedundefinedjs run let schedule ={
"meetups": [
{"title":"Conference","date":"2017-11-30T12:00:00.000Z"},
{"title":"Birthday","date":"2017-04-18T12:00:00.000Z"} ]
}`;undefinedundefined
schedule = JSON.parse(schedule, function(key, value) { if (key == ‘date') return new Date(value); return value; });
undefinedundefinedundefinedundefined! alert( schedule.meetups[1].date.getDate() ); // works! undefinedundefined/! undefinedundefined
undefinedundefinednull.undefinedundefined
toJSON, then it is called by
undefinedundefinedJSON.stringify.undefinedundefined
Let's return to functions and study them more in-depth.
undefinedundefinedOur first topic will be undefinedundefinedrecursion.undefinedundefined
undefinedundefinedIf you are not new to programming, then it is probably familiar and you could skip this chapter.
undefinedundefinedRecursion is a programming pattern that is useful in situations when a task can be naturally split into several tasks of the same kind, but simpler. Or when a task can be simplified into an easy action plus a simpler variant of the same task. Or, as we'll see soon, to deal with certain data structures.
undefinedundefinedWhen a function solves a task, in the process it can call many other functions. A partial case of this is when a function calls undefinedundefineditself. That's called undefinedundefinedrecursion.undefinedundefined
undefinedundefinedFor something simple to start
with - let's write a function
undefinedundefinedpow(x, n) that raises
undefinedundefinedx to a natural power of
undefinedundefinedn. In other words, multiplies
undefinedundefinedx by itself
undefinedundefinedn times.undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedpow(undefinedundefined2undefinedundefined,undefinedundefined2) undefinedundefined=undefinedundefined4undefinedundefinedundefinedundefinedundefinedundefinedpow(undefinedundefined2undefinedundefined,undefinedundefined3) undefinedundefined=undefinedundefined8undefinedundefinedundefinedundefinedundefinedundefinedpow(undefinedundefined2undefinedundefined,undefinedundefined4) undefinedundefined=undefinedundefined16undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
There are two ways to implement it.
undefinedundefinedIterative thinking: the
undefinedundefinedfor loop:undefinedundefined
run function pow(x, n) { let result = 1;
undefinedundefined// multiply result by x n times in the loop for (let i = 0; i < n; i++) { result *= x; }
undefinedundefinedreturn result; }
undefinedundefinedalert( pow(2, 3) ); // 8
undefinedundefinedRecursive thinking: simplify the task and call self:
undefinedundefinedrun function pow(x, n) { if (n == 1) { return x; } else { return x * pow(x, n - 1); } }
undefinedundefinedalert( pow(2, 3) ); // 8
undefinedundefinedPlease note how the recursive variant is fundamentally different.
undefinedundefinedWhen
undefinedundefinedpow(x, n) is called, the
execution splits into two branches:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedif nundefinedundefined==undefinedundefined1undefinedundefined= xundefinedundefinedundefinedundefined /undefinedundefinedundefinedundefinedpow(xundefinedundefined, n) undefinedundefined=undefinedundefinedundefinedundefined \ undefinedundefinedundefinedundefinedelseundefinedundefined= x undefinedundefined*undefinedundefinedpow(xundefinedundefined, n undefinedundefined-undefinedundefined1)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
n == 1, then everything is
trivial. It is called undefinedundefinedthe base of
recursion, because it immediately produces the obvious result:
undefinedundefinedpow(x, 1) equals
undefinedundefinedx.undefinedundefinedpow(x, n) as
undefinedundefinedx * pow(x, n - 1). In maths,
one would write
undefinedundefinedxundefinedundefinedn = x * xundefinedundefinedn-1undefinedundefined.
This is called undefinedundefineda recursive step: we
transform the task into a simpler action (multiplication by
undefinedundefinedx) and a simpler call of the
same task (undefinedundefinedpow with lower
undefinedundefinedn). Next steps simplify it
further and further until undefinedundefinedn
reaches undefinedundefined1.undefinedundefined
We can also say
that
undefinedundefinedpowundefinedundefinedrecursively
calls itself till
undefinedundefinedn == 1.undefinedundefined
For example, to
calculate undefinedundefinedpow(2, 4) the recursive
variant does these steps:undefinedundefined
pow(2, 4) = 2 * pow(2, 3)undefinedundefined
pow(2, 3) = 2 * pow(2, 2)undefinedundefined
pow(2, 2) = 2 * pow(2, 1)undefinedundefined
pow(2, 1) = 2undefinedundefined
So, the recursion reduces a function call to a simpler one, and then - to even more simpler, and so on, until the result becomes obvious.
undefinedundefinedsmart header="Recursion is usually shorter" A recursive solution is usually shorter than an iterative one.
undefinedundefinedHere we can rewrite the same using the conditional operator
undefinedundefined? instead of
undefinedundefinedif to make
undefinedundefinedpow(x, n) more terse and still
very readable:undefinedundefined
undefinedundefinedjs run function pow(x, n) { return (n == 1) ? x : (x * pow(x, n - 1)); }
undefinedundefined
The maximal number of
nested calls (including the first one) is called
undefinedundefinedrecursion depth. In our case, it will
be exactly undefinedundefinedn.undefinedundefined
The maximal recursion depth is limited by JavaScript engine. We can rely on it being 10000, some engines allow more, but 100000 is probably out of limit for the majority of them. There are automatic optimizations that help alleviate this ("tail calls optimizations"), but they are not yet supported everywhere and work only in simple cases.
undefinedundefinedThat limits the application of recursion, but it still remains very wide. There are many tasks where recursive way of thinking gives simpler code, easier to maintain.
undefinedundefinedNow let's examine how recursive calls work. For that we'll look under the hood of functions.
undefinedundefinedThe information about the process of execution of a running function is stored in its undefinedundefinedexecution context.undefinedundefined
undefinedundefinedThe undefinedundefinedexecution
context is an internal data structure that contains
details about the execution of a function: where the control
flow is now, the current variables, the value of
undefinedundefinedthis (we don't use it here) and
few other internal details.undefinedundefined
One function call has exactly one execution context associated with it.
undefinedundefinedWhen a function makes a nested call, the following happens:
undefinedundefinedLet's see what
happens during the undefinedundefinedpow(2, 3)
call.undefinedundefined
In the beginning of the call
undefinedundefinedpow(2, 3) the execution context
will store variables:
undefinedundefinedx = 2, n = 3, the execution flow
is at line undefinedundefined1 of the
function.undefinedundefined
We can sketch it as:
undefinedundefinedThat's when the function starts to execute.
The condition undefinedundefinedn == 1 is falsy, so
the flow continues into the second branch of
undefinedundefinedif:undefinedundefined
run function pow(x, n) { if (n == 1) { return x; } else { undefinedundefined! return x * pow(x, n - 1); undefinedundefined/! } }undefinedundefined
undefinedundefinedalert( pow(2, 3) );
undefinedundefinedThe variables are same, but the line changes, so the context is now:
undefinedundefinedTo calculate
undefinedundefinedx * pow(x, n - 1), we need to
make a subcall of undefinedundefinedpow with new
arguments
undefinedundefinedpow(2, 2).undefinedundefined
To do a nested call, JavaScript remembers the current execution context in the undefinedundefinedexecution context stack.undefinedundefined
undefinedundefinedHere we
call the same function undefinedundefinedpow, but
it absolutely doesn't matter. The process is the same for all
functions:undefinedundefined
Here's the context stack when we entered
the subcall
undefinedundefinedpow(2, 2):undefinedundefined
The new current execution context is on top (and bold), and previous remembered contexts are below.
undefinedundefinedWhen we finish the subcall - it is easy to resume the previous context, because it keeps both variables and the exact place of the code where it stopped.
undefinedundefinedundefinedundefinedHere in the picture we use the word "line", as in our example there's only one subcall in line, but generally a single line of code may contain multiple subcalls, like `pow(…) + pow(…) + somethingElse(…)`.
So it would be more precise to say that the execution resumes "immediately after the subcall".undefinedundefined
undefinedundefinedThe process repeats: a new subcall is made at line
undefinedundefined5, now with arguments
undefinedundefinedx=2,
undefinedundefinedn=1.undefinedundefined
A new execution context is created, the previous one is pushed on top of the stack:
undefinedundefinedThere are 2 old contexts now and 1 currently
running for
undefinedundefinedpow(2, 1).undefinedundefined
During the execution of
undefinedundefinedpow(2, 1), unlike before, the
condition undefinedundefinedn == 1 is truthy, so
the first branch of undefinedundefinedif
works:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedpow(xundefinedundefined, n) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (n undefinedundefined==undefinedundefined1) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedreturn xundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined } else {undefinedundefinedundefinedundefinedundefinedundefined return x undefinedundefined*undefinedundefined powundefinedundefined(undefinedundefinedx, n - 1undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
There are no more nested calls, so the
function finishes, returning
undefinedundefined2.undefinedundefined
As the function finishes, its execution context is not needed anymore, so it's removed from the memory. The previous one is restored off the top of the stack:
undefinedundefinedThe execution of
undefinedundefinedpow(2, 2) is resumed. It has the
result of the subcall undefinedundefinedpow(2, 1),
so it also can finish the evaluation of
undefinedundefinedx * pow(x, n - 1), returning
undefinedundefined4.undefinedundefined
Then the previous context is restored:
undefinedundefinedWhen it finishes, we have a result of
undefinedundefinedpow(2, 3) = 8.undefinedundefined
The recursion depth in this case was: undefinedundefined3.undefinedundefined
undefinedundefinedAs we can see from the illustrations above, recursion depth equals the maximal number of context in the stack.
undefinedundefinedNote the memory requirements.
Contexts take memory. In our case, raising to the power of
undefinedundefinedn actually requires the memory
for undefinedundefinedn contexts, for all lower
values of undefinedundefinedn.undefinedundefined
A loop-based algorithm is more memory-saving:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedpow(xundefinedundefined, n) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet result undefinedundefined=undefinedundefined1undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedlet i undefinedundefined=undefinedundefined0undefinedundefined; i undefinedundefined< nundefinedundefined; iundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefined result undefinedundefined*= xundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedreturn resultundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The iterative
undefinedundefinedpow uses a single context
changing undefinedundefinedi and
undefinedundefinedresult in the process. Its memory
requirements are small, fixed and do not depend on
undefinedundefinedn.undefinedundefined
undefinedundefinedAny recursion can be rewritten as a loop. The loop variant usually can be made more effective.undefinedundefined
undefinedundefined…But sometimes the rewrite is non-trivial, especially when function uses different recursive subcalls depending on conditions and merges their results or when the branching is more intricate. And the optimization may be unneeded and totally not worth the efforts.
undefinedundefinedRecursion can give a shorter code, easier to understand and support. Optimizations are not required in every place, mostly we need a good code, that's why it's used.
undefinedundefinedAnother great application of the recursion is a recursive traversal.
undefinedundefinedImagine, we have a company. The staff structure can be presented as an object:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet company undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedsalesundefinedundefined: [undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined'John'undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedsalaryundefinedundefined:undefinedundefined1000undefinedundefinedundefinedundefinedundefinedundefined},undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined'Alice'undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedsalaryundefinedundefined:undefinedundefined1600undefinedundefinedundefinedundefinedundefinedundefined}]undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefineddevelopmentundefinedundefined:undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedsitesundefinedundefined: [undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined'Peter'undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedsalaryundefinedundefined:undefinedundefined2000undefinedundefinedundefinedundefinedundefinedundefined},undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined'Alex'undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedsalaryundefinedundefined:undefinedundefined1800undefinedundefinedundefinedundefinedundefinedundefined}]undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedinternalsundefinedundefined: [undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined'Jack'undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedsalaryundefinedundefined:undefinedundefined1300undefinedundefinedundefinedundefinedundefinedundefined}]undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In other words, a company has departments.
undefinedundefinedsales department has 2
employees: John and Alice.undefinedundefineddevelopment has two branches:
undefinedundefinedsites and
undefinedundefinedinternals. Each of them has
their own staff.undefinedundefinedIt is also possible that when a subdepartment grows, it divides into subsubdepartments (or teams).
undefinedundefinedFor instance, the
undefinedundefinedsites department in the
future may be split into teams for
undefinedundefinedsiteA and
undefinedundefinedsiteB. And they, potentially,
can split even more. That's not on the picture, just
something to have in mind.undefinedundefined
Now let's say we want a function to get the sum of all salaries. How can we do that?
undefinedundefinedAn iterative approach is not easy, because the structure is not
simple. The first idea may be to make a
undefinedundefinedfor loop over
undefinedundefinedcompany with nested subloop over
1st level departments. But then we need more nested subloops to
iterate over the staff in 2nd level departments like
undefinedundefinedsites… And then another subloop
inside those for 3rd level departments that might appear in the
future? If we put 3-4 nested subloops in the code to traverse a
single object, it becomes rather ugly.undefinedundefined
Let's try recursion.
undefinedundefinedAs we can see, when our function gets a department to sum, there are two possible cases:
undefinedundefinedN
subdepartments - then we can make
undefinedundefinedN recursive calls to get the
sum for each of the subdeps and combine the
results.undefinedundefinedThe 1st case is the base of recursion, the trivial case, when we get an array.
undefinedundefinedThe 2nd case when we get an object is the recursive step. A complex task is split into subtasks for smaller departments. They may in turn split again, but sooner or later the split will finish at (1).
undefinedundefinedThe algorithm is probably even easier to read from the code:
undefinedundefinedrun let company = { // the same object, compressed for brevity sales: [{name: ‘John', salary: 1000}, {name: ‘Alice', salary: 1600 }], development: { sites: [{name: ‘Peter', salary: 2000}, {name: ‘Alex', salary: 1800 }], internals: [{name: ‘Jack', salary: 1300}] } };
undefinedundefined// The function to do the job undefinedundefined! function sumSalaries(department) { if (Array.isArray(department)) { // case (1) return department.reduce((prev, current) => prev + current.salary, 0); // sum the array } else { // case (2) let sum = 0; for (let subdep of Object.values(department)) { sum += sumSalaries(subdep); // recursively call for subdepartments, sum the results } return sum; } } undefinedundefined/!undefinedundefined
undefinedundefinedalert(sumSalaries(company)); // 7700
undefinedundefinedThe code is short and easy to understand (hopefully?). That's the power of recursion. It also works for any level of subdepartment nesting.
undefinedundefinedHere's the diagram of calls:
undefinedundefinedWe can easily see the principle: for an
object undefinedundefined{...} subcalls are made,
while arrays undefinedundefined[...] are the
"leaves" of the recursion tree, they give immediate
result.undefinedundefined
Note that the code uses smart features that we've covered before:
undefinedundefinedarr.reduce explained in the
chapter undefinedundefinedinfo:array-methods to get the sum of the
array.undefinedundefinedfor(val of Object.values(obj))
to iterate over object values:
undefinedundefinedObject.values returns an array
of them.undefinedundefinedA recursive (recursively-defined) data structure is a structure that replicates itself in parts.
undefinedundefinedWe've just seen it in the example of a company structure above.
undefinedundefinedA company undefinedundefineddepartment is: - Either an array of people. - Or an object with undefinedundefineddepartments.undefinedundefined
undefinedundefinedFor web-developers there are much better-known examples: HTML and XML documents.
undefinedundefinedIn the HTML document, an undefinedundefinedHTML-tag may contain a list of: - Text pieces. - HTML-comments. - Other undefinedundefinedHTML-tags (that in turn may contain text pieces/comments or other tags etc).undefinedundefined
undefinedundefinedThat's once again a recursive definition.
undefinedundefinedFor better understanding, we'll cover one more recursive structure named "Linked list" that might be a better alternative for arrays in some cases.
undefinedundefinedImagine, we want to store an ordered list of objects.
undefinedundefinedThe natural choice would be an array:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet arr undefinedundefined= [obj1undefinedundefined, obj2undefinedundefined, obj3]undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…But there's a problem with arrays. The
"delete element" and "insert element" operations are expensive.
For instance, undefinedundefinedarr.unshift(obj)
operation has to renumber all elements to make room for a new
undefinedundefinedobj, and if the array is big, it
takes time. Same with
undefinedundefinedarr.shift().undefinedundefined
The only structural modifications that do
not require mass-renumbering are those that operate with the end
of array: undefinedundefinedarr.push/pop. So an
array can be quite slow for big queues, when we have to work
with the beginning.undefinedundefined
Alternatively, if we really need fast insertion/deletion, we can choose another data structure called a undefinedundefinedlinked list.undefinedundefined
undefinedundefinedThe
undefinedundefinedlinked list element is recursively
defined as an object with: -
undefinedundefinedvalue. -
undefinedundefinednext property referencing the
next undefinedundefinedlinked list element or
undefinedundefinednull if that's the
end.undefinedundefined
For instance:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet list undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedvalueundefinedundefined:undefinedundefined1undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinednextundefinedundefined:undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedvalueundefinedundefined:undefinedundefined2undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinednextundefinedundefined:undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedvalueundefinedundefined:undefinedundefined3undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinednextundefinedundefined:undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedvalueundefinedundefined:undefinedundefined4undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinednextundefinedundefined:undefinedundefinednullundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Graphical representation of the list:
undefinedundefinedAn alternative code for creation:
undefinedundefined
undefinedundefinedjs no-beautify let list = { value: 1 }; list.next = { value: 2 }; list.next.next = { value: 3 }; list.next.next.next = { value: 4 }; list.next.next.next.next = null;undefinedundefined
Here we can even more clearly see that
there are multiple objects, each one has the
undefinedundefinedvalue and
undefinedundefinednext pointing to the neighbour.
The undefinedundefinedlist variable is the first
object in the chain, so following
undefinedundefinednext pointers from it we can
reach any element.undefinedundefined
The list can be easily split into multiple parts and later joined back:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet secondList undefinedundefined=undefinedundefinedlist.undefinedundefinednext.undefinedundefinednextundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedlist.undefinedundefinednext.undefinedundefinednextundefinedundefined=undefinedundefinednullundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
To join:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlist.undefinedundefinednext.undefinedundefinednextundefinedundefined= secondListundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
And surely we can insert or remove items in any place.
undefinedundefinedFor instance, to prepend a new value, we need to update the head of the list:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet list undefinedundefined=undefinedundefined{undefinedundefinedvalueundefinedundefined:undefinedundefined1undefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedlist.undefinedundefinednextundefinedundefined=undefinedundefined{undefinedundefinedvalueundefinedundefined:undefinedundefined2undefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedlist.undefinedundefinednext.undefinedundefinednextundefinedundefined=undefinedundefined{undefinedundefinedvalueundefinedundefined:undefinedundefined3undefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedlist.undefinedundefinednext.undefinedundefinednext.undefinedundefinednextundefinedundefined=undefinedundefined{undefinedundefinedvalueundefinedundefined:undefinedundefined4undefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefined// prepend the new value to the listundefinedundefinedundefinedundefinedlist undefinedundefined=undefinedundefined{undefinedundefinedvalueundefinedundefined:undefinedundefined"new item"undefinedundefined,undefinedundefinednextundefinedundefined: list undefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
To remove a value from the middle, change
undefinedundefinednext of the previous
one:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlist.undefinedundefinednextundefinedundefined=undefinedundefinedlist.undefinedundefinednext.undefinedundefinednextundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
We
made undefinedundefinedlist.next jump over
undefinedundefined1 to value
undefinedundefined2. The value
undefinedundefined1 is now excluded from the chain.
If it's not stored anywhere else, it will be automatically
removed from the memory.undefinedundefined
Unlike arrays, there's no mass-renumbering, we can easily rearrange elements.
undefinedundefinedNaturally, lists are not always better than arrays. Otherwise everyone would use only lists.
undefinedundefinedThe main drawback is that we
can't easily access an element by its number. In an array that's
easy: undefinedundefinedarr[n] is a direct
reference. But in the list we need to start from the first item
and go
undefinedundefinednextundefinedundefinedN
times to get the Nth element.undefinedundefined
…But we don't always need such operations. For instance, when we need a queue or even a undefinedundefineddeque - the ordered structure that must allow very fast adding/removing elements from both ends, but access to its middle is not needed.undefinedundefined
undefinedundefined
Lists can be enhanced: - We can add property
undefinedundefinedprev in addition to
undefinedundefinednext to reference the previous
element, to move back easily. - We can also add a variable named
undefinedundefinedtail referencing the last element
of the list (and update it when adding/removing elements from
the end). - …The data structure may vary according to our
needs.undefinedundefined
Terms: - undefinedundefinedRecursion is a programming term that means calling a function from itself. Recursive functions can be used to solve tasks in elegant ways.undefinedundefined
undefinedundefinedundefinedundefinedWhen a function calls itself, that's called a *recursion step*. The *basis* of recursion is function arguments that make the task so simple that the function does not make further calls.undefinedundefined
undefinedundefinedA undefinedundefinedrecursively-defined data structure is a data structure that can be defined using itself.undefinedundefined
undefinedundefinedFor instance, the linked list can be defined as a data structure consisting of an object referencing a list (or null).
undefinedundefinedundefinedundefinedundefinedundefinedlist undefinedundefined=undefinedundefined{ valueundefinedundefined, next undefinedundefined-> list undefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Trees like HTML elements tree or the department tree from this chapter are also naturally recursive: they branch and every branch can have other branches.
undefinedundefinedRecursive functions can be
used to walk them as we've seen in the
undefinedundefinedsumSalary
example.undefinedundefined
Any recursive function can be rewritten into an iterative one. And that's sometimes required to optimize stuff. But for many tasks a recursive solution is fast enough and easier to write and support.
undefinedundefinedThis part of the tutorial is about core JavaScript, the language itself.
undefinedundefinedBut we
need a working environment to run our scripts and, since this
book is online, the browser is a good choice. We'll keep the
amount of browser-specific commands (like
undefinedundefinedalert) to a minimum so that you
don't spend time on them if you plan to concentrate on another
environment (like Node.js). We'll focus on JavaScript in the
browser in the undefinedundefinednext part of
the tutorial.undefinedundefined
So
first, let's see how we attach a script to a webpage. For
server-side environments (like Node.js), you can execute the
script with a command like
undefinedundefined"node my.js".undefinedundefined
JavaScript programs can be inserted
almost anywhere into an HTML document using the
undefinedundefined<script>
tag.undefinedundefined
For instance:
run height=100 <!DOCTYPE HTML> undefinedundefinedundefinedundefined undefinedundefinedBefore the script…
undefinedundefined!undefinedundefined undefinedundefinedundefinedundefined/!undefinedundefined
undefinedundefined…After the script.
undefinedundefined undefinedundefined undefinedundefinedundefinedundefined
undefinedundefinedYou can run the example by clicking the "Play" button in the right-top corner of the box above.undefinedundefined
undefinedundefinedThe undefinedundefined<script> tag contains JavaScript code which is
automatically executed when the browser processes the tag.undefinedundefined
The undefinedundefined<script> tag has a
few attributes that are rarely used nowadays but can still be found in old code:undefinedundefined
type attribute:
undefinedundefined<script undefinedundefinedtype=…>undefinedundefinedundefinedundefined
type. Usually it was undefinedundefinedtype="text/javascript". It's not
required anymore. Also, the modern HTML standard totally changed the meaning of this attribute. Now, it can be used
for JavaScript modules. But that's an advanced topic, we'll talk about modules in another part of the tutorial.
undefinedundefinedlanguage attribute:
undefinedundefined<script undefinedundefinedlanguage=…>undefinedundefinedundefinedundefined
In really
ancient books and guides, you may find comments inside undefinedundefined<script> tags, like
this:undefinedundefined
undefinedundefinedhtml no-beautify <script type="text/javascript"><!-- ... //--></script>undefinedundefined
This trick isn't used in modern JavaScript. These comments hide JavaScript code from old
browsers that didn't know how to process the undefinedundefined<script> tag. Since browsers
released in the last 15 years don't have this issue, this kind of comment can help you identify really old
code.undefinedundefined
If we have a lot of JavaScript code, we can put it into a separate file.
undefinedundefinedScript files are attached to HTML with the
undefinedundefinedsrc attribute:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined<scriptundefinedundefined src=undefinedundefined"/path/to/script.js"undefinedundefined></script>undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Here, undefinedundefined/path/to/script.js is an absolute path to the script
from the site root. One can also provide a relative path from the current page. For instance,
undefinedundefinedsrc="script.js" would mean a file undefinedundefined"script.js" in the
current folder.undefinedundefined
We can give a full URL as well. For instance:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined<scriptundefinedundefined src=undefinedundefined"https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"undefinedundefined></script>undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
To attach several scripts, use multiple tags:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined<scriptundefinedundefined src=undefinedundefined"/js/script1.js"undefinedundefined></script>undefinedundefinedundefinedundefinedundefinedundefined<scriptundefinedundefined src=undefinedundefined"/js/script2.js"undefinedundefined></script>undefinedundefinedundefinedundefined…undefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedAs a rule, only the simplest scripts are put into HTML. More complex ones reside in separate files.
The benefit of a separate file is that the browser will download it and store it in its [cache](https://en.wikipedia.org/wiki/Web_cache).
Other pages that reference the same script will take it from the cache instead of downloading it, so the file is actually downloaded only once.
That reduces traffic and makes pages faster.undefinedundefined
undefinedundefinedwarn header="Ifsrcundefinedundefinedis set, the script content is ignored." A singleundefinedundefined
undefinedundefinedundefinedundefined
We must choose either an external undefinedundefined<script src="…"> or a
regular undefinedundefined<script> with code.undefinedundefined
The example above can be split into two scripts to work:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined<scriptundefinedundefined src=undefinedundefined"file.js"undefinedundefined></script>undefinedundefinedundefinedundefinedundefinedundefined<script>undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined1)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined</script>undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefined
<script> tag to add JavaScript code to a page.undefinedundefinedtype and undefinedundefinedlanguage attributes
are not required.undefinedundefined<script src="path/to/script.js"></script>.undefinedundefinedThere is much more to learn about browser scripts and their interaction with the webpage. But let's keep in mind that this part of the tutorial is devoted to the JavaScript language, so we shouldn't distract ourselves with browser-specific implementations of it. We'll be using the browser as a way to run JavaScript, which is very convenient for online reading, but only one of many.
undefinedundefinedMany JavaScript built-in functions support an arbitrary number of arguments.
undefinedundefinedFor instance:
undefinedundefinedMath.max(arg1, arg2, ..., argN) - returns the
greatest of the arguments.undefinedundefinedObject.assign(dest, src1, ..., srcN) - copies properties from
undefinedundefinedsrc1..N into undefinedundefineddest.undefinedundefinedIn this chapter we'll learn how to do the same. And also, how to pass arrays to such functions as parameters.
undefinedundefined...undefinedundefinedA function can be called with any number of arguments, no matter how it is defined.
undefinedundefinedLike here: run function sum(a, b) { return a + b; }
undefinedundefinedalert( sum(1, 2, 3, 4, 5) );
undefinedundefinedThere will be no error because of "excessive" arguments. But of course in the result only the first two will be counted.
undefinedundefinedThe rest of the parameters can be included in the function
definition by using three dots undefinedundefined... followed by the name of the array that will contain
them. The dots literally mean "gather the remaining parameters into an array".undefinedundefined
For instance, to gather all arguments into array undefinedundefinedargs:undefinedundefined
run function sumAll(…args) { // args is the name for the array let sum = 0;
undefinedundefinedfor (let arg of args) sum += arg;
undefinedundefinedreturn sum; }
undefinedundefinedalert( sumAll(1) ); // 1 alert( sumAll(1, 2) ); // 3 alert( sumAll(1, 2, 3) ); // 6
undefinedundefinedWe can choose to get the first parameters as variables, and gather only the rest.
undefinedundefinedHere the first two arguments go into variables and the rest go into
undefinedundefinedtitles array:undefinedundefined
run function showName(firstName, lastName, …titles) { alert( firstName + ''' ''' + lastName ); // Julius Caesar
undefinedundefined// the rest go into titles array // i.e. titles = ["Consul", "Imperator"] alert( titles[0] ); // Consul alert( titles[1] ); // Imperator alert( titles.length ); // 2 }
undefinedundefinedshowName("Julius", "Caesar", "Consul", "Imperator");
undefinedundefinedwarn header="The rest parameters must be at the end" The rest parameters gather all remaining arguments, so the following does not make sense and causes an error:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedf(arg1undefinedundefined, ...undefinedundefinedrestundefinedundefined, arg2) undefinedundefined{undefinedundefined// arg2 after ...rest ?!undefinedundefinedundefinedundefinedundefinedundefined// errorundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The undefinedundefined...rest must always be last.
undefinedundefined
There is also a special array-like object named undefinedundefinedarguments that
contains all arguments by their index.undefinedundefined
For instance:
undefinedundefinedrun function showName() { alert( arguments.length ); alert( arguments[0] ); alert( arguments[1] );
undefinedundefined// it's iterable // for(let arg of arguments) alert(arg); }
undefinedundefined// shows: 2, Julius, Caesar showName("Julius", "Caesar");
undefinedundefined// shows: 1, Ilya, undefined (no second argument) showName("Ilya");
undefinedundefinedIn old times, rest parameters did not exist in the language, and using
undefinedundefinedarguments was the only way to get all arguments of the function. And it still works, we
can find it in the old code.undefinedundefined
But the downside is that although
undefinedundefinedarguments is both array-like and iterable, it's not an array. It does not support array
methods, so we can't call undefinedundefinedarguments.map(...) for example.undefinedundefined
Also, it always contains all arguments. We can't capture them partially, like we did with rest parameters.
undefinedundefinedSo when we need these features, then rest parameters are preferred.
undefinedundefined
undefinedundefinedsmart header="Arrow functions do not have"arguments"undefinedundefined" If we access thearguments`
object from an arrow function, it takes them from the outer "normal" function.undefinedundefined
Here's an example:
undefinedundefinedrun function f() { let showArg = () => alert(arguments[0]); showArg(); }
undefinedundefinedf(1); // 1
undefinedundefinedundefinedundefined
As we remember, arrow functions don't have their own `this`. Now we know they don't have the special `arguments` object either.undefinedundefined
undefinedundefinedWe've just seen how to get an array from the list of parameters.
undefinedundefinedBut sometimes we need to do exactly the reverse.
undefinedundefinedFor instance, there's a built-in function undefinedundefinedMath.max that returns the greatest number from a list:undefinedundefined
undefinedundefinedundefinedundefinedjs run alert( Math.max(3, 5, 1) ); // 5undefinedundefined
Now let's say we have an array undefinedundefined[3, 5, 1]. How do we call
undefinedundefinedMath.max with it?undefinedundefined
Passing it "as is" won't
work, because undefinedundefinedMath.max expects a list of numeric arguments, not a single
array:undefinedundefined
run let arr = [3, 5, 1];
undefinedundefinedundefinedundefined! alert( Math.max(arr) ); // NaN undefinedundefined/! undefinedundefined
undefinedundefinedAnd surely we can't manually list items in the code
undefinedundefinedMath.max(arr[0], arr[1], arr[2]), because we may be unsure how many there are. As our
script executes, there could be a lot, or there could be none. And that would get ugly.undefinedundefined
undefinedundefinedSpread syntax to the rescue! It looks similar to rest parameters, also
using undefinedundefined..., but does quite the opposite.undefinedundefined
When
undefinedundefined...arr is used in the function call, it "expands" an iterable object
undefinedundefinedarr into the list of arguments.undefinedundefined
For
undefinedundefinedMath.max:undefinedundefined
run let arr = [3, 5, 1];
undefinedundefinedalert( Math.max(…arr) ); // 5 (spread turns array into a list of arguments)
undefinedundefinedWe also can pass multiple iterables this way:
undefinedundefinedrun let arr1 = [1, -2, 3, 4]; let arr2 = [8, 3, -8, 1];
undefinedundefinedalert( Math.max(…arr1, …arr2) ); // 8
undefinedundefinedWe can even combine the spread syntax with normal values:
undefinedundefinedrun let arr1 = [1, -2, 3, 4]; let arr2 = [8, 3, -8, 1];
undefinedundefinedalert( Math.max(1, …arr1, 2, …arr2, 25) ); // 25
undefinedundefinedAlso, the spread syntax can be used to merge arrays:
undefinedundefinedrun let arr = [3, 5, 1]; let arr2 = [8, 9, 15];
undefinedundefinedundefinedundefined! let merged = [0, …arr, 2, …arr2]; undefinedundefined/!undefinedundefined
undefinedundefinedalert(merged); // 0,3,5,1,2,8,9,15 (0, then arr, then 2, then arr2)
undefinedundefinedIn the examples above we used an array to demonstrate the spread syntax, but any iterable will do.
undefinedundefinedFor instance, here we use the spread syntax to turn the string into array of characters:
undefinedundefinedrun let str = "Hello";
undefinedundefinedalert( […str] ); // H,e,l,l,o
undefinedundefinedThe spread syntax internally uses iterators to gather elements, the same way as
undefinedundefinedfor..of does.undefinedundefined
So, for a string,
undefinedundefinedfor..of returns characters and undefinedundefined...str becomes
undefinedundefined"H","e","l","l","o". The list of characters is passed to array initializer
undefinedundefined[...str].undefinedundefined
For this particular task we could
also use undefinedundefinedArray.from, because it converts an iterable (like a string) into an
array:undefinedundefined
run let str = "Hello";
undefinedundefined// Array.from converts an iterable into an array alert( Array.from(str) ); // H,e,l,l,o
undefinedundefinedThe result is the same as undefinedundefined[...str].undefinedundefined
But there's a subtle difference between undefinedundefinedArray.from(obj) and
undefinedundefined[...obj]:undefinedundefined
Array.from operates on both array-likes and iterables.undefinedundefinedSo,
for the task of turning something into an array, undefinedundefinedArray.from tends to be more
universal.undefinedundefined
Remember when we talked about undefinedundefinedObject.assign()undefinedundefinedin the past?undefinedundefined
It is possible to do the same thing with the spread syntax.
undefinedundefinedrun let arr = [1, 2, 3];
undefinedundefinedundefinedundefined! let arrCopy = […arr]; // spread the array into a list of parameters // then put the result into a new array undefinedundefined/!undefinedundefined
undefinedundefined// do the arrays have the same contents? alert(JSON.stringify(arr) === JSON.stringify(arrCopy)); // true
undefinedundefined// are the arrays equal? alert(arr === arrCopy); // false (not same reference)
undefinedundefined// modifying our initial array does not modify the copy: arr.push(4); alert(arr); // 1, 2, 3, 4 alert(arrCopy); // 1, 2, 3
undefinedundefinedNote that it is possible to do the same thing to make a copy of an object:
undefinedundefinedrun let obj = { a: 1, b: 2, c: 3 };
undefinedundefinedundefinedundefined! let objCopy = { …obj }; // spread the object into a list of parameters // then return the result in a new object undefinedundefined/!undefinedundefined
undefinedundefined// do the objects have the same contents? alert(JSON.stringify(obj) === JSON.stringify(objCopy)); // true
undefinedundefined// are the objects equal? alert(obj === objCopy); // false (not same reference)
undefinedundefined// modifying our initial object does not modify the copy: obj.d = 4; alert(JSON.stringify(obj)); // {"a":1,"b":2,"c":3,"d":4} alert(JSON.stringify(objCopy)); // {"a":1,"b":2,"c":3}
undefinedundefinedThis way of copying an object is much shorter than
undefinedundefinedlet objCopy = Object.assign({}, obj) or for an array
undefinedundefinedlet arrCopy = Object.assign([], arr) so we prefer to use it whenever we
can.undefinedundefined
When we see
undefinedundefined"..." in the code, it is either rest parameters or the spread syntax.undefinedundefined
There's an easy way to distinguish between them:
undefinedundefined... is at the end of function parameters, it's "rest parameters" and gathers
the rest of the list of arguments into an array.undefinedundefined... occurs in a function call or alike, it's called a "spread syntax" and expands an
array into a list.undefinedundefinedUse patterns:
undefinedundefinedTogether they help to travel between a list and an array of parameters with ease.
undefinedundefinedAll arguments of a function call are also available in
"old-style" undefinedundefinedarguments: array-like iterable object.undefinedundefined
JavaScript is a very function-oriented language. It gives us a lot of freedom. A function can be created at any moment, passed as an argument to another function, and then called from a totally different place of code later.
undefinedundefinedWe already know that a function can access variables outside of it ("outer" variables).
undefinedundefinedBut what happens if outer variables change since a function is created? Will the function get newer values or the old ones?
undefinedundefinedAnd what if a function is passed along as a parameter and called from another place of code, will it get access to outer variables at the new place?
undefinedundefinedLet's expand our knowledge to understand these scenarios and more complex ones.
undefinedundefined
``undefinedundefinedsmart header="We'll talk aboutlet/constundefinedundefinedvariables here" In JavaScript, there are 3 ways to declare a variable:letundefinedundefined,constundefinedundefined(the modern ones), andvar`
(the remnant of the past).undefinedundefined
let variables in examples.undefinedundefinedconst, behave the same, so this article is about
undefinedundefinedconst too.undefinedundefinedvar has some notable differences, they will be covered in the article
undefinedundefinedinfo:var.
undefinedundefinedIf a variable is declared inside a code block undefinedundefined{...}, it's only
visible inside that block.undefinedundefined
For example:
undefinedundefinedrun { // do some job with local variables that should not be seen outside
undefinedundefinedlet message = "Hello"; // only visible in this block
undefinedundefinedalert(message); // Hello }
undefinedundefinedalert(message); // Error: message is not defined
undefinedundefinedWe can use this to isolate a piece of code that does its own task, with variables that only belong to it:
undefinedundefinedrun { // show message let message = "Hello"; alert(message); }
undefinedundefined{ // show another message let message = "Goodbye"; alert(message); }
undefinedundefined
undefinedundefinedsmart header="There'd be an error without blocks" Please note, without separate blocks there would be an error, if we uselet`
with the existing variable name:undefinedundefined
run // show message let message = "Hello"; alert(message);
undefinedundefined// show another message undefinedundefined! let message = "Goodbye"; // Error: variable already declared undefinedundefined/! alert(message);undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedFor undefinedundefinedif,
undefinedundefinedfor, undefinedundefinedwhile and so on, variables declared in
undefinedundefined{...} are also only visible inside:undefinedundefined
run if (true) { let phrase = "Hello!";
undefinedundefinedalert(phrase); // Hello! }
undefinedundefinedalert(phrase); // Error, no such variable!
undefinedundefinedHere, after undefinedundefinedif finishes, the
undefinedundefinedalert below won't see the undefinedundefinedphrase, hence the
error.undefinedundefined
That's great, as it allows us to create block-local variables,
specific to an undefinedundefinedif branch.undefinedundefined
The similar thing
holds true for undefinedundefinedfor and undefinedundefinedwhile loops:undefinedundefined
run for (let i = 0; i < 3; i++) { // the variable i is only visible inside this for alert(i); // 0, then 1, then 2 }
undefinedundefinedalert(i); // Error, no such variable
undefinedundefinedVisually, undefinedundefinedlet i is outside of
undefinedundefined{...}. But the undefinedundefinedfor construct is special here: the
variable, declared inside it, is considered a part of the block.undefinedundefined
A function is called "nested" when it is created inside another function.
undefinedundefinedIt is easily possible to do this with JavaScript.
undefinedundefinedWe can use it to organize our code, like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedsayHiBye(firstNameundefinedundefined, lastName) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// helper nested function to use belowundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedgetFullName() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturn firstName undefinedundefined+undefinedundefined" "undefinedundefined+ lastNameundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefined"Hello, "undefinedundefined+undefinedundefinedgetFullName() )undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefined"Bye, "undefinedundefined+undefinedundefinedgetFullName() )undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Here the undefinedundefinednested function
undefinedundefinedgetFullName() is made for convenience. It can access the outer variables and so can
return the full name. Nested functions are quite common in JavaScript.undefinedundefined
What's much more interesting, a nested function can be returned: either as a property of a new object or as a result by itself. It can then be used somewhere else. No matter where, it still has access to the same outer variables.
undefinedundefinedBelow, undefinedundefinedmakeCounter creates the "counter" function that returns the
next number on each invocation:undefinedundefined
run function makeCounter() { let count = 0;
undefinedundefinedreturn function() { return count++; }; }
undefinedundefinedlet counter = makeCounter();
undefinedundefinedalert( counter() ); // 0 alert( counter() ); // 1 alert( counter() ); // 2
undefinedundefinedDespite being simple, slightly modified variants of that code have practical uses, for instance, as a undefinedundefinedrandom number generator to generate random values for automated tests.undefinedundefined
undefinedundefinedHow does this work? If we create multiple counters, will they be independent? What's going on with the variables here?
undefinedundefinedUnderstanding such things is great for the overall knowledge of JavaScript and beneficial for more complex scenarios. So let's go a bit in-depth.
undefinedundefinedwarn header="Here be dragons!" The in-depth technical explanation lies ahead.
undefinedundefinedAs far as I'd like to avoid low-level language details, any understanding without them would be lacking and incomplete, so get ready.
undefinedundefinedFor clarity, the explanation is split into multiple steps.
undefinedundefinedIn JavaScript, every running function, code block
undefinedundefined{...}, and the script as a whole have an internal (hidden) associated object known as
the undefinedundefinedLexical Environment.undefinedundefined
The Lexical Environment object consists of two parts:
undefinedundefinedthis).undefinedundefinedundefinedundefinedA
"variable" is just a property of the special internal object, undefinedundefinedEnvironment Record. "To
get or change a variable" means "to get or change a property of that
object".undefinedundefinedundefinedundefined
In this simple code without functions, there is only one Lexical Environment:
undefinedundefinedThis is the so-called undefinedundefinedglobal Lexical Environment, associated with the whole script.undefinedundefined
undefinedundefinedOn the picture above,
the rectangle means Environment Record (variable store) and the arrow means the outer reference. The global Lexical
Environment has no outer reference, that's why the arrow points to
undefinedundefinednull.undefinedundefined
As the code starts executing and goes on, the Lexical Environment changes.
undefinedundefinedHere's a little bit longer code:
undefinedundefinedRectangles on the right-hand side demonstrate how the global Lexical Environment changes during the execution:
undefinedundefinedlet. It's almost the same as if the variable didn't
exist.undefinedundefinedlet phrase definition appears. There's no assignment yet, so its value is
undefinedundefinedundefined. We can use the variable from this point forward.undefinedundefinedphrase is assigned a value.undefinedundefinedphrase changes the value.undefinedundefinedEverything looks simple for now, right?
undefinedundefinedsmart header="Lexical Environment is a specification object" "Lexical Environment" is a specification object: it only exists "theoretically" in the undefinedundefinedlanguage specification to describe how things work. We can't get this object in our code and manipulate it directly.undefinedundefined
undefinedundefinedJavaScript engines also may optimize it, discard variables that are unused to save memory and perform other internal tricks, as long as the visible behavior remains as described.
undefinedundefinedA function is also a value, like a variable.
undefinedundefinedundefinedundefinedThe difference is that a Function Declaration is instantly fully initialized.undefinedundefined
undefinedundefinedWhen a
Lexical Environment is created, a Function Declaration immediately becomes a ready-to-use function (unlike
undefinedundefinedlet, that is unusable till the declaration).undefinedundefined
That's why we can use a function, declared as Function Declaration, even before the declaration itself.
undefinedundefinedFor example, here's the initial state of the global Lexical Environment when we add a function:
undefinedundefinedundefinedundefinedundefinedundefined
Naturally, this behavior only applies to Function Declarations, not Function Expressions where we
assign a function to a variable, such as undefinedundefinedlet say = function(name)....undefinedundefined
When a function runs, at the beginning of the call, a new Lexical Environment is created automatically to store local variables and parameters of the call.
undefinedundefinedFor instance, for
undefinedundefinedsay("John"), it looks like this (the execution is at the line, labelled with an
arrow):undefinedundefined
undefinedundefinedundefinedundefined
During the function call we have two Lexical Environments: the inner one (for the function call) and the outer one (global):
undefinedundefinedsay. It has a single property:
undefinedundefinedname, the function argument. We called undefinedundefinedsay("John"), so
the value of the undefinedundefinedname is undefinedundefined"John".undefinedundefined
phrase variable and the function itself.undefinedundefinedThe inner Lexical Environment has a reference to the undefinedundefinedouter
one.undefinedundefined
undefinedundefinedWhen the code wants to access a variable - the inner Lexical Environment is searched first, then the outer one, then the more outer one and so on until the global one.undefinedundefined
undefinedundefinedIf a variable is not found anywhere, that's an error
in strict mode (without undefinedundefineduse strict, an assignment to a non-existing variable creates a
new global variable, for compatibility with old code).undefinedundefined
In this example the search proceeds as follows:
undefinedundefinedname
variable, the undefinedundefinedalert inside undefinedundefinedsay finds it immediately in
the inner Lexical Environment.undefinedundefinedphrase, then there is no undefinedundefinedphrase locally, so it follows
the reference to the outer Lexical Environment and finds it there.undefinedundefinedLet's return to the undefinedundefinedmakeCounter example.undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedmakeCounter() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet count undefinedundefined=undefinedundefined0undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturn countundefinedundefined++;undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet counter undefinedundefined=undefinedundefinedmakeCounter()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
At the beginning of each undefinedundefinedmakeCounter() call, a new Lexical
Environment object is created, to store variables for this undefinedundefinedmakeCounter
run.undefinedundefined
So we have two nested Lexical Environments, just like in the example above:
undefinedundefinedundefinedundefinedundefinedundefined
What's different is that, during the execution of undefinedundefinedmakeCounter(), a
tiny nested function is created of only one line: undefinedundefinedreturn count++. We don't run it yet,
only create.undefinedundefined
All functions remember the Lexical Environment in which they
were made. Technically, there's no magic here: all functions have the hidden property named
undefinedundefined[[Environment]], that keeps the reference to the Lexical Environment where the function
was created:undefinedundefined
undefinedundefinedundefinedundefined
So,
undefinedundefinedcounter.[[Environment]] has the reference to undefinedundefined{count: 0}
Lexical Environment. That's how the function remembers where it was created, no matter where it's called. The
undefinedundefined[[Environment]] reference is set once and forever at function creation
time.undefinedundefined
Later, when undefinedundefinedcounter() is called, a new
Lexical Environment is created for the call, and its outer Lexical Environment reference is taken from
undefinedundefinedcounter.[[Environment]]:undefinedundefined
undefinedundefinedundefinedundefined
Now when
the code inside undefinedundefinedcounter() looks for undefinedundefinedcount variable, it
first searches its own Lexical Environment (empty, as there are no local variables there), then the Lexical
Environment of the outer undefinedundefinedmakeCounter() call, where it finds and changes
it.undefinedundefined
undefinedundefinedA variable is updated in the Lexical Environment where it lives.undefinedundefined
undefinedundefinedHere's the state after the execution:
undefinedundefinedundefinedundefinedundefinedundefined
If we call undefinedundefinedcounter() multiple times, the
undefinedundefinedcount variable will be increased to undefinedundefined2,
undefinedundefined3 and so on, at the same place.undefinedundefined
smart header="Closure" There is a general programming term "closure", that developers generally should know.
undefinedundefinedA undefinedundefinedclosure is a function that remembers its outer variables and can access them. In some languages, that's not possible, or a function should be written in a special way to make it happen. But as explained above, in JavaScript, all functions are naturally closures (there is only one exception, to be covered in undefinedundefinedinfo:new-function).undefinedundefined
undefinedundefinedThat is: they automatically remember
where they were created using a hidden undefinedundefined[[Environment]] property, and then their code
can access outer variables.undefinedundefined
When on an interview, a frontend developer gets
a question about "what's a closure?", a valid answer would be a definition of the closure and an explanation that all
functions in JavaScript are closures, and maybe a few more words about technical details: the
undefinedundefined[[Environment]] property and how Lexical Environments work.
undefinedundefined
Usually, a Lexical Environment is removed from memory with all the variables after the function call finishes. That's because there are no references to it. As any JavaScript object, it's only kept in memory while it's reachable.
undefinedundefinedHowever, if there's a nested function that is still reachable after the end of a function, then it
has undefinedundefined[[Environment]] property that references the lexical environment.undefinedundefined
In that case the Lexical Environment is still reachable even after the completion of the function, so it stays alive.
undefinedundefinedFor example:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedf() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet value undefinedundefined=undefinedundefined123undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(value)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet g undefinedundefined=undefinedundefinedf()undefinedundefined;undefinedundefined// g.[[Environment]] stores a reference to the Lexical Environmentundefinedundefinedundefinedundefinedundefinedundefined// of the corresponding f() callundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Please note that if undefinedundefinedf() is called many times, and resulting
functions are saved, then all corresponding Lexical Environment objects will also be retained in memory. In the code
below, all 3 of them:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedf() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet value undefinedundefined=undefinedundefinedMath.undefinedundefinedrandom()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedfunction() undefinedundefined{undefinedundefinedalert(value)undefinedundefined;undefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// 3 functions in array, every one of them links to Lexical Environmentundefinedundefinedundefinedundefinedundefinedundefined// from the corresponding f() runundefinedundefinedundefinedundefinedundefinedundefinedlet arr undefinedundefined= [undefinedundefinedf()undefinedundefined,undefinedundefinedf()undefinedundefined,undefinedundefinedf()]undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
A Lexical Environment object dies when it becomes unreachable (just like any other object). In other words, it exists only while there's at least one nested function referencing it.
undefinedundefinedIn
the code below, after the nested function is removed, its enclosing Lexical Environment (and hence the
undefinedundefinedvalue) is cleaned from memory:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedf() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet value undefinedundefined=undefinedundefined123undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(value)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet g undefinedundefined=undefinedundefinedf()undefinedundefined;undefinedundefined// while g function exists, the value stays in memoryundefinedundefinedundefinedundefinedundefinedundefinedg undefinedundefined=undefinedundefinednullundefinedundefined;undefinedundefined// ...and now the memory is cleaned upundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
As we've seen, in theory while a function is alive, all outer variables are also retained.
undefinedundefinedBut in practice, JavaScript engines try to optimize that. They analyze variable usage and if it's obvious from the code that an outer variable is not used - it is removed.
undefinedundefinedundefinedundefinedAn important side effect in V8 (Chrome, Edge, Opera) is that such variable will become unavailable in debugging.undefinedundefined
undefinedundefinedTry running the example below in Chrome with the Developer Tools open.
undefinedundefinedWhen
it pauses, in the console type undefinedundefinedalert(value).undefinedundefined
run function f() { let value = Math.random();
undefinedundefinedfunction g() { debugger; // in console: type alert(value); No such variable! }
undefinedundefinedreturn g; }
undefinedundefinedlet g = f(); g();
undefinedundefinedAs you could see - there is no such variable! In theory, it should be accessible, but the engine optimized it out.
undefinedundefinedThat may lead to funny (if not such time-consuming) debugging issues. One of them - we can see a same-named outer variable instead of the expected one:
undefinedundefinedrun global let value = "Surprise!";
undefinedundefinedfunction f() { let value = "the closest value";
undefinedundefinedfunction g() { debugger; // in console: type alert(value); Surprise! }
undefinedundefinedreturn g; }
undefinedundefinedlet g = f(); g();
undefinedundefinedThis feature of V8 is good to know. If you are debugging with Chrome/Edge/Opera, sooner or later you will meet it.
undefinedundefinedThat is not a bug in the debugger, but rather a special feature of V8. Perhaps it will be changed sometime. You can always check for it by running the examples on this page.
undefinedundefinedsmart header="This article is for understanding old scripts" The information in this article is useful for understanding old scripts.
undefinedundefinedThat's not how we write a new code.
undefinedundefinedIn the very first chapter about undefinedundefinedvariables, we mentioned three ways of variable declaration:undefinedundefined
undefinedundefinedletundefinedundefinedconstundefinedundefinedvarundefinedundefinedThe
undefinedundefinedvar declaration is similar to undefinedundefinedlet. Most of the time we
can replace undefinedundefinedlet by undefinedundefinedvar or vice-versa and expect things
to work:undefinedundefined
undefinedundefinedjs run var message = "Hi"; alert(message); // Hiundefinedundefined
But internally undefinedundefinedvar is a very different beast, that originates from
very old times. It's generally not used in modern scripts, but still lurks in the old ones.undefinedundefined
If you don't plan on meeting such scripts you may even skip this chapter or postpone it.
undefinedundefinedOn the other hand, it's important to understand differences when migrating old scripts from
undefinedundefinedvar to undefinedundefinedlet, to avoid odd errors.undefinedundefined
Variables, declared
with undefinedundefinedvar, are either function-scoped or global-scoped. They are visible through
blocks.undefinedundefined
For instance:
undefinedundefinedrun if (true) { var test = true; // use "var" instead of "let" }
undefinedundefinedundefinedundefined! alert(test); // true, the variable lives after if undefinedundefined/! undefinedundefined
undefinedundefinedAs undefinedundefinedvar ignores code blocks, we've got a
global variable undefinedundefinedtest.undefinedundefined
If we used
undefinedundefinedlet test instead of undefinedundefinedvar test, then the variable would
only be visible inside undefinedundefinedif:undefinedundefined
run if (true) { let test = true; // use "let" }
undefinedundefinedundefinedundefined! alert(test); // ReferenceError: test is not defined undefinedundefined/! undefinedundefined
undefinedundefinedThe same thing for loops: undefinedundefinedvar cannot be
block- or loop-local:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedvar i undefinedundefined=undefinedundefined0undefinedundefined; i undefinedundefined<undefinedundefined10undefinedundefined; iundefinedundefined++) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedvar one undefinedundefined=undefinedundefined1undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedalert(i)undefinedundefined;undefinedundefined// 10, "i" is visible after loop, it's a global variableundefinedundefinedundefinedundefinedundefinedundefinedalert(one)undefinedundefined;undefinedundefined// 1, "one" is visible after loop, it's a global variableundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
If a code block is inside a function, then undefinedundefinedvar becomes a
function-level variable:undefinedundefined
run function sayHi() { if (true) { var phrase = "Hello"; }
undefinedundefinedalert(phrase); // works }
undefinedundefinedsayHi(); alert(phrase); // ReferenceError: phrase is not defined
undefinedundefinedAs we can see, undefinedundefinedvar pierces through
undefinedundefinedif, undefinedundefinedfor or other code blocks. That's because a long time
ago in JavaScript, blocks had no Lexical Environments, and undefinedundefinedvar is a remnant of
that.undefinedundefined
If we declare the same variable with undefinedundefinedlet twice in the same scope,
that's an error:undefinedundefined
undefinedundefinedjs run let user; let user; // SyntaxError: 'user' has already been declaredundefinedundefined
With undefinedundefinedvar, we can redeclare a variable any number of times. If we
use undefinedundefinedvar with an already-declared variable, it's just ignored:undefinedundefined
run var user = "Pete";
undefinedundefinedvar user = "John"; // this "var" does nothing (already declared) // …it doesn't trigger an error
undefinedundefinedalert(user); // John
undefinedundefinedundefinedundefinedvar declarations are processed when the function starts
(or script starts for globals).undefinedundefined
In other words,
undefinedundefinedvar variables are defined from the beginning of the function, no matter where the
definition is (assuming that the definition is not in the nested function).undefinedundefined
So this code:
undefinedundefinedrun function sayHi() { phrase = "Hello";
undefinedundefinedalert(phrase);
undefinedundefinedundefinedundefined! var phrase; undefinedundefined/! } sayHi(); undefinedundefined
undefinedundefined…Is technically the same as this (moved
undefinedundefinedvar phrase above):undefinedundefined
run function sayHi() { undefinedundefined! var phrase; undefinedundefined/!undefinedundefined
undefinedundefinedphrase = "Hello";
undefinedundefinedalert(phrase); } sayHi();
undefinedundefined…Or even as this (remember, code blocks are ignored):
undefinedundefinedrun function sayHi() { phrase = "Hello"; // (*)
undefinedundefinedundefinedundefined! if (false) { var phrase; } undefinedundefined/!undefinedundefined
undefinedundefinedalert(phrase); } sayHi();
undefinedundefinedPeople also call such behavior "hoisting" (raising), because all
undefinedundefinedvar are "hoisted" (raised) to the top of the function.undefinedundefined
So in the example above, undefinedundefinedif (false) branch never executes, but that
doesn't matter. The undefinedundefinedvar inside it is processed in the beginning of the function, so at
the moment of undefinedundefined(*) the variable exists.undefinedundefined
undefinedundefinedDeclarations are hoisted, but assignments are not.undefinedundefined
undefinedundefinedThat's best demonstrated with an example:
undefinedundefinedrun function sayHi() { alert(phrase);
undefinedundefinedundefinedundefined! var phrase = "Hello"; undefinedundefined/! }undefinedundefined
undefinedundefinedsayHi();
undefinedundefinedThe line undefinedundefinedvar phrase = "Hello" has two actions in
it:undefinedundefined
varundefinedundefined=.undefinedundefinedThe declaration is processed at the start of function execution ("hoisted"), but the assignment always works at the place where it appears. So the code works essentially like this:
undefinedundefinedrun function sayHi() { undefinedundefined! var phrase; // declaration works at the start… undefinedundefined/!undefinedundefined
undefinedundefinedalert(phrase); // undefined
undefinedundefinedundefinedundefined! phrase = "Hello"; // …assignment - when the execution reaches it. undefinedundefined/! }undefinedundefined
undefinedundefinedsayHi();
undefinedundefinedBecause all undefinedundefinedvar declarations are processed at the function
start, we can reference them at any place. But variables are undefined until the assignments.undefinedundefined
In both examples above, undefinedundefinedalert runs without an error, because the
variable undefinedundefinedphrase exists. But its value is not yet assigned, so it shows
undefinedundefinedundefined.undefinedundefined
In the past, as there was only undefinedundefinedvar, and it has no block-level
visibility, programmers invented a way to emulate it. What they did was called "immediately-invoked function
expressions" (abbreviated as IIFE).undefinedundefined
That's not something we should use nowadays, but you can find them in old scripts.
undefinedundefinedAn IIFE looks like this:
undefinedundefinedrun (function() {
undefinedundefinedvar message = "Hello";
undefinedundefinedalert(message); // Hello
undefinedundefined})();
undefinedundefinedHere, a Function Expression is created and immediately called. So the code executes right away and has its own private variables.
undefinedundefinedThe Function Expression is wrapped with parenthesis
undefinedundefined(function {...}), because when JavaScript engine encounters
undefinedundefined"function" in the main code, it understands it as the start of a Function Declaration.
But a Function Declaration must have a name, so this kind of code will give an error:undefinedundefined
run // Tries to declare and immediately call a function function() { // <- SyntaxError: Function statements require a function name
undefinedundefinedvar message = "Hello";
undefinedundefinedalert(message); // Hello
undefinedundefined}();
undefinedundefinedEven if we say: "okay, let's add a name", that won't work, as JavaScript does not allow Function Declarations to be called immediately:
undefinedundefinedrun // syntax error because of parentheses below function go() {
undefinedundefined}(); // <- can't call Function Declaration immediately
undefinedundefinedSo, the parentheses around the function is a trick to show JavaScript that the function is created in the context of another expression, and hence it's a Function Expression: it needs no name and can be called immediately.
undefinedundefinedThere exist other ways besides parentheses to tell JavaScript that we mean a Function Expression:
undefinedundefinedrun // Ways to create IIFE
undefinedundefined(function() { alert("Parentheses around the function"); }undefinedundefined!)undefinedundefined/!();undefinedundefined
undefinedundefined(function() { alert("Parentheses around the whole thing"); }()undefinedundefined!)undefinedundefined/!;undefinedundefined
undefinedundefinedundefinedundefined!!undefinedundefined/!function() { alert("Bitwise NOT operator starts the expression"); }();undefinedundefined
undefinedundefinedundefinedundefined!+undefinedundefined/!function() { alert("Unary plus starts the expression"); }(); undefinedundefined
undefinedundefinedIn all the above cases we declare a Function Expression and run it immediately. Let's note again: nowadays there's no reason to write such code.
undefinedundefinedThere are two main differences of undefinedundefinedvar compared to
undefinedundefinedlet/const:undefinedundefined
var variables have no block scope, their visibility is scoped to current function, or
global, if declared outside function.undefinedundefinedvar
declarations are processed at function start (script start for globals).undefinedundefinedThere's one more very minor difference related to the global object, that we'll cover in the next chapter.
undefinedundefinedThese differences make undefinedundefinedvar worse than
undefinedundefinedlet most of the time. Block-level variables is such a great thing. That's why
undefinedundefinedlet was introduced in the standard long ago, and is now a major way (along with
undefinedundefinedconst) to declare a variable.undefinedundefined
The global object provides variables and functions that are available anywhere. By default, those that are built into the language or the environment.
undefinedundefinedIn
a browser it is named undefinedundefinedwindow, for Node.js it is undefinedundefinedglobal,
for other environments it may have another name.undefinedundefined
Recently,
undefinedundefinedglobalThis was added to the language, as a standardized name for a global object, that
should be supported across all environments. It's supported in all major browsers.undefinedundefined
We'll use undefinedundefinedwindow here, assuming that our environment is a browser.
If your script may run in other environments, it's better to use undefinedundefinedglobalThis
instead.undefinedundefined
All properties of the global object can be accessed directly:
undefinedundefined
undefinedundefinedjs run alert("Hello"); // is the same as window.alert("Hello");undefinedundefined
In a browser, global functions and variables declared with undefinedundefinedvar (not
undefinedundefinedlet/const!) become the property of the global object:undefinedundefined
run untrusted refresh var gVar = 5;
undefinedundefinedalert(window.gVar); // 5 (became a property of the global object)
undefinedundefinedThe same effect have function declarations (statements with
undefinedundefinedfunction keyword in the main code flow, not function expressions).undefinedundefined
Please don't rely on that! This behavior exists for compatibility reasons. Modern scripts use undefinedundefinedJavaScript modules where such a thing doesn't happen.undefinedundefined
undefinedundefinedIf we used undefinedundefinedlet instead, such thing wouldn't
happen:undefinedundefined
run untrusted refresh let gLet = 5;
undefinedundefinedalert(window.gLet); // undefined (doesn't become a property of the global object)
undefinedundefinedIf a value is so important that you'd like to make it available globally, write it directly as a property:
undefinedundefinedrun undefinedundefined! // make current user information global, to let all scripts access it window.currentUser = { name: "John" }; undefinedundefined/!undefinedundefined
undefinedundefined// somewhere else in code alert(currentUser.name); // John
undefinedundefined// or, if we have a local variable with the name "currentUser" // get it from window explicitly (safe!) alert(window.currentUser.name); // John
undefinedundefinedThat said, using global variables is generally discouraged. There should be as few global variables as possible. The code design where a function gets "input" variables and produces certain "outcome" is clearer, less prone to errors and easier to test than if it uses outer or global variables.
undefinedundefinedWe use the global object to test for support of modern language features.
undefinedundefinedFor instance, test if a built-in
undefinedundefinedPromise object exists (it doesn't in really old browsers):
undefinedundefinedjs run if (!window.Promise) { alert("Your browser is really old!"); }undefinedundefined
If there's none (say, we're in an old browser), we can create "polyfills": add functions that are not supported by the environment, but exist in the modern standard.
undefinedundefined
undefinedundefinedjs run if (!window.Promise) { window.Promise = ... // custom implementation of the modern language feature }undefinedundefined
The global object holds variables that should be available everywhere.
That includes JavaScript built-ins, such as undefinedundefinedArray and environment-specific values,
such as undefinedundefinedwindow.innerHeight - the window height in the browser.undefinedundefinedThe global object has a universal name
undefinedundefinedglobalThis.undefinedundefined
window (browser) and undefinedundefinedglobal
(Node.js).undefinedundefinedvar become a property of the global object.undefinedundefinedTo make our code future-proof and easier to understand, we should access properties of the
global object directly, as undefinedundefinedwindow.x.undefinedundefined
As we already know, a function in JavaScript is a value.
undefinedundefinedEvery value in JavaScript has a type. What type is a function?
undefinedundefinedIn JavaScript, functions are objects.
undefinedundefinedA good way to imagine functions is as callable "action objects". We can not only call them, but also treat them as objects: add/remove properties, pass by reference etc.
undefinedundefinedFunction objects contain some useable properties.
undefinedundefinedFor instance, a function's name is accessible as the "name" property:
undefinedundefinedrun function sayHi() { alert("Hi"); }
undefinedundefinedalert(sayHi.name); // sayHi
undefinedundefinedWhat's kind of funny, the name-assigning logic is smart. It also assigns the correct name to a function even if it's created without one, and then immediately assigned:
undefinedundefinedrun let sayHi = function() { alert("Hi"); };
undefinedundefinedalert(sayHi.name); // sayHi (there's a name!)
undefinedundefinedIt also works if the assignment is done via a default value:
undefinedundefinedrun function f(sayHi = function() {}) { alert(sayHi.name); // sayHi (works!) }
undefinedundefinedf();
undefinedundefinedIn the specification, this feature is called a "contextual name". If the function does not provide one, then in an assignment it is figured out from the context.
undefinedundefinedObject methods have names too:
undefinedundefinedrun let user = {
undefinedundefinedsayHi() { // … },
undefinedundefinedsayBye: function() { // … }
undefinedundefined}
undefinedundefinedalert(user.sayHi.name); // sayHi alert(user.sayBye.name); // sayBye
undefinedundefinedThere's no magic though. There are cases when there's no way to figure out the right name. In that case, the name property is empty, like here:
undefinedundefinedrun // function created inside array let arr = [function() {}];
undefinedundefinedalert( arr[0].name ); // undefinedundefined
In practice, however, most functions do have a name.
undefinedundefinedThere is another built-in property "length" that returns the number of function parameters, for instance:
undefinedundefinedrun function f1(a) {} function f2(a, b) {} function many(a, b, …more) {}
undefinedundefinedalert(f1.length); // 1 alert(f2.length); // 2 alert(many.length); // 2
undefinedundefinedHere we can see that rest parameters are not counted.
undefinedundefinedThe
undefinedundefinedlength property is sometimes used for undefinedundefinedintrospection in functions that operate on other
functions.undefinedundefined
For instance, in the code below the
undefinedundefinedask function accepts a undefinedundefinedquestion to ask and an arbitrary
number of undefinedundefinedhandler functions to call.undefinedundefined
Once a user provides their answer, the function calls the handlers. We can pass two kinds of handlers:
undefinedundefinedTo call undefinedundefinedhandler the right way, we examine
the undefinedundefinedhandler.length property.undefinedundefined
The idea is that we have a simple, no-arguments handler syntax for positive cases (most frequent variant), but are able to support universal handlers as well:
undefinedundefinedrun function ask(question, …handlers) { let isYes = confirm(question);
undefinedundefinedfor(let handler of handlers) { if (handler.length == 0) { if (isYes) handler(); } else { handler(isYes); } }
undefinedundefined}
undefinedundefined// for positive answer, both handlers are called // for negative answer, only the second one ask("Question?", () => alert(‘You said yes'), result => alert(result));
undefinedundefinedThis is a particular case of so-called undefinedundefinedpolymorphism - treating arguments
differently depending on their type or, in our case depending on the undefinedundefinedlength. The idea
does have a use in JavaScript libraries.undefinedundefined
We can also add properties of our own.
undefinedundefinedHere we add the
undefinedundefinedcounter property to track the total calls count:undefinedundefined
run function sayHi() { alert("Hi");
undefinedundefinedundefinedundefined! // let's count how many times we run sayHi.counter++; undefinedundefined/! } sayHi.counter = 0; // initial valueundefinedundefined
undefinedundefinedsayHi(); // Hi sayHi(); // Hi
undefinedundefinedalert(
undefinedundefinedCalled ${sayHi.counter} times ); // Called 2 times
undefinedundefined
``undefinedundefinedwarn header="A property is not a variable" A property assigned to a function likesayHi.counter
=
0undefinedundefineddoes *not* define a local variablecounterundefinedundefinedinside it. In other words, a propertycounterundefinedundefinedand a variablelet
counter` are two unrelated things.undefinedundefined
We can treat a function as an object, store properties in it, but that has no effect on its execution. Variables are not function properties and vice versa. These are just parallel worlds.
undefinedundefinedFunction properties can replace closures sometimes. For instance, we can rewrite the counter function example from the chapter undefinedundefinedinfo:closure to use a function property:undefinedundefined
undefinedundefinedrun function makeCounter() { // instead of: // let count = 0
undefinedundefinedfunction counter() { return counter.count++; };
undefinedundefinedcounter.count = 0;
undefinedundefinedreturn counter; }
undefinedundefinedlet counter = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1
undefinedundefinedThe undefinedundefinedcount is now stored in the function directly, not in its
outer Lexical Environment.undefinedundefined
Is it better or worse than using a closure?
undefinedundefinedThe main difference is that if the value of undefinedundefinedcount lives in an outer
variable, then external code is unable to access it. Only nested functions may modify it. And if it's bound to a
function, then such a thing is possible:undefinedundefined
run function makeCounter() {
undefinedundefinedfunction counter() { return counter.count++; };
undefinedundefinedcounter.count = 0;
undefinedundefinedreturn counter; }
undefinedundefinedlet counter = makeCounter();
undefinedundefinedundefinedundefined! counter.count = 10; alert( counter() ); // 10 undefinedundefined/! undefinedundefined
undefinedundefinedSo the choice of implementation depends on our aims.
undefinedundefinedNamed Function Expression, or NFE, is a term for Function Expressions that have a name.
undefinedundefinedFor instance, let's take an ordinary Function Expression:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet sayHi undefinedundefined=undefinedundefinedfunction(who) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`Hello, undefinedundefined${whoundefinedundefined}undefinedundefined`)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
And add a name to it:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet sayHi undefinedundefined=undefinedundefinedfunctionundefinedundefined*!*funcundefinedundefined*undefinedundefined/!undefinedundefined*(undefinedundefinedwhoundefinedundefined)undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined alertundefinedundefined(undefinedundefined`Hello, ${who}`undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Did we achieve anything here? What's the purpose of that additional
undefinedundefined"func" name?undefinedundefined
First let's note, that we still
have a Function Expression. Adding the name undefinedundefined"func" after
undefinedundefinedfunction did not make it a Function Declaration, because it is still created as a part
of an assignment expression.undefinedundefined
Adding such a name also did not break anything.
undefinedundefinedThe function is still available as undefinedundefinedsayHi():undefinedundefined
``undefinedundefinedjs run let sayHi = function *!*func*/!*(who) { alert(Hello,
${who}`); };undefinedundefined
sayHi("John"); // Hello, John
undefinedundefinedThere are two special things about the name undefinedundefinedfunc, that are the
reasons for it:undefinedundefined
For instance, the function undefinedundefinedsayHi below
calls itself again with undefinedundefined"Guest" if no undefinedundefinedwho is
provided:undefinedundefined
``undefinedundefinedjs run let sayHi = function *!*func*/!*(who) { if (who) { alert(Hello,
${who}`); } else { undefinedundefined! func("Guest"); // use func to re-call itself
undefinedundefined/! } };undefinedundefined
sayHi(); // Hello, Guest
undefinedundefined// But this won't work: func(); // Error, func is not defined (not visible outside of the function)
undefinedundefinedWhy do we use undefinedundefinedfunc? Maybe just use
undefinedundefinedsayHi for the nested call?undefinedundefined
Actually, in most cases we can:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet sayHi undefinedundefined=undefinedundefinedfunction(who) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (who) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`Hello, undefinedundefined${whoundefinedundefined}undefinedundefined`)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedelseundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedsayHi(undefinedundefined"Guest")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The problem with that code is that undefinedundefinedsayHi may change in the
outer code. If the function gets assigned to another variable instead, the code will start to give
errors:undefinedundefined
``undefinedundefinedjs run let sayHi = function(who) { if (who) { alert(Hello, ${who}`); } else {
undefinedundefined! sayHi("Guest"); // Error: sayHi is not a function undefinedundefined/! }
};undefinedundefined
let welcome = sayHi; sayHi = null;
undefinedundefinedwelcome(); // Error, the nested sayHi call doesn't work any more!
undefinedundefinedThat happens because the function takes undefinedundefinedsayHi from its outer
lexical environment. There's no local undefinedundefinedsayHi, so the outer variable is used. And at the
moment of the call that outer undefinedundefinedsayHi is
undefinedundefinednull.undefinedundefined
The optional name which we can put into the Function Expression is meant to solve exactly these kinds of problems.
undefinedundefinedLet's use it to fix our code:
undefinedundefined
``undefinedundefinedjs run let sayHi = function *!*func*/!*(who) { if (who) { alert(Hello,
${who}`); } else { undefinedundefined! func("Guest"); // Now all fine undefinedundefined/! }
};undefinedundefined
let welcome = sayHi; sayHi = null;
undefinedundefinedwelcome(); // Hello, Guest (nested call works)
undefinedundefinedNow it works, because the name undefinedundefined"func" is function-local. It is
not taken from outside (and not visible there). The specification guarantees that it will always reference the current
function.undefinedundefined
The outer code still has its variable
undefinedundefinedsayHi or undefinedundefinedwelcome. And
undefinedundefinedfunc is an "internal function name", how the function can call itself
internally.undefinedundefined
smart header="There's no such thing for Function Declaration" The "internal name" feature described here is only available for Function Expressions, not for Function Declarations. For Function Declarations, there is no syntax for adding an "internal" name.
undefinedundefinedSometimes, when we need a reliable internal name, it's the reason to rewrite a Function Declaration to Named Function Expression form.
undefinedundefinedFunctions are objects.
undefinedundefinedHere we covered their properties:
undefinedundefinedname -
the function name. Usually taken from the function definition, but if there's none, JavaScript tries to guess it
from the context (e.g. an assignment).undefinedundefinedlength - the number of arguments in the function definition. Rest parameters are not
counted.undefinedundefinedIf the function is declared as a Function Expression (not in the main code flow), and it carries the name, then it is called a Named Function Expression. The name can be used inside to reference itself, for recursive calls or such.
undefinedundefinedAlso, functions may carry additional properties. Many well-known JavaScript libraries make great use of this feature.
undefinedundefinedThey create a "main" function and attach many other "helper" functions to it. For instance, the
undefinedundefinedjQuery library creates a function named
undefinedundefined$. The undefinedundefinedlodash library creates a
function undefinedundefined_, and then adds undefinedundefined_.clone,
undefinedundefined_.keyBy and other properties to it (see the undefinedundefineddocs when you want to learn more about them). Actually, they do it to lessen
their pollution of the global space, so that a single library gives only one global variable. That reduces the
possibility of naming conflicts.undefinedundefined
So, a function can do a useful job by itself and also carry a bunch of other functionality in properties.
undefinedundefinedThere's one more way to create a function. It's rarely used, but sometimes there's no alternative.
undefinedundefinedThe syntax for creating a function:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet func undefinedundefined=undefinedundefinednewundefinedundefinedFunction ([arg1undefinedundefined, arg2undefinedundefined, ...undefinedundefinedargN]undefinedundefined, functionBody)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The function is created with the arguments undefinedundefinedarg1...argN and the
given undefinedundefinedfunctionBody.undefinedundefined
It's easier to understand by looking at an example. Here's a function with two arguments:
undefinedundefinedrun let sum = new Function(‘a', ‘b', ‘return a + b');
undefinedundefinedalert( sum(1, 2) ); // 3
undefinedundefinedAnd here there's a function without arguments, with only the function body:
undefinedundefinedrun let sayHi = new Function(‘alert("Hello")''');
undefinedundefinedsayHi(); // Hello
undefinedundefinedThe major difference from other ways we've seen is that the function is created literally from a string, that is passed at run time.
undefinedundefinedAll previous declarations required us, programmers, to write the function code in the script.
undefinedundefinedBut undefinedundefinednew Function allows
to turn any string into a function. For example, we can receive a new function from a server and then execute
it:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet str undefinedundefined= ... undefinedundefinedreceive the code undefinedundefinedfrom a server undefinedundefineddynamically ...undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet func undefinedundefined=undefinedundefinednewundefinedundefinedFunction(str)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedfunc()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
It is used in very specific cases, like when we receive code from a server, or to dynamically compile a function from a template, in complex web-applications.
undefinedundefinedUsually, a function remembers where it was born in the special property
undefinedundefined[[Environment]]. It references the Lexical Environment from where it's created (we
covered that in the chapter undefinedundefinedinfo:closure).undefinedundefined
But when a function is created using undefinedundefinednew Function, its
undefinedundefined[[Environment]] is set to reference not the current Lexical Environment, but the global
one.undefinedundefined
So, such function doesn't have access to outer variables, only to the global ones.
undefinedundefinedrun function getFunc() { let value = "test";
undefinedundefinedundefinedundefined! let func = new Function(‘alert(value)'''); undefinedundefined/!undefinedundefined
undefinedundefinedreturn func; }
undefinedundefinedgetFunc()(); // error: value is not defined
undefinedundefinedCompare it with the regular behavior:
undefinedundefinedrun function getFunc() { let value = "test";
undefinedundefinedundefinedundefined! let func = function() { alert(value); }; undefinedundefined/!undefinedundefined
undefinedundefinedreturn func; }
undefinedundefinedgetFunc()(); // undefinedundefined!"test"undefinedundefined/!, from the Lexical Environment of getFunc undefinedundefined
undefinedundefinedThis special feature of undefinedundefinednew Function looks
strange, but appears very useful in practice.undefinedundefined
Imagine that we must create a function from a string. The code of that function is not known at the time of writing the script (that's why we don't use regular functions), but will be known in the process of execution. We may receive it from the server or from another source.
undefinedundefinedOur new function needs to interact with the main script.
undefinedundefinedWhat if it could access the outer variables?
undefinedundefinedThe problem is that before JavaScript is published to production, it's compressed using a undefinedundefinedminifier - a special program that shrinks code by removing extra comments, spaces and - what's important, renames local variables into shorter ones.undefinedundefined
undefinedundefinedFor instance, if a function has
undefinedundefinedlet userName, minifier replaces it with undefinedundefinedlet a (or
another letter if this one is occupied), and does it everywhere. That's usually a safe thing to do, because the
variable is local, nothing outside the function can access it. And inside the function, minifier replaces every
mention of it. Minifiers are smart, they analyze the code structure, so they don't break anything. They're not just a
dumb find-and-replace.undefinedundefined
So if undefinedundefinednew Function had
access to outer variables, it would be unable to find renamed
undefinedundefineduserName.undefinedundefined
undefinedundefinedIf
undefinedundefinednew Function had access to outer variables, it would have problems with
minifiers.undefinedundefinedundefinedundefined
Besides, such code would be architecturally bad and prone to errors.
undefinedundefinedTo pass something to a function, created as
undefinedundefinednew Function, we should use its arguments.undefinedundefined
The syntax:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet func undefinedundefined=undefinedundefinednewundefinedundefinedFunction ([arg1undefinedundefined, arg2undefinedundefined, ...undefinedundefinedargN]undefinedundefined, functionBody)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
For historical reasons, arguments can also be given as a comma-separated list.
undefinedundefinedThese three declarations mean the same:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinednewundefinedundefinedFunction(undefinedundefined'a'undefinedundefined,undefinedundefined'b'undefinedundefined,undefinedundefined'return a + b')undefinedundefined;undefinedundefined// basic syntaxundefinedundefinedundefinedundefinedundefinedundefinednewundefinedundefinedFunction(undefinedundefined'a,b'undefinedundefined,undefinedundefined'return a + b')undefinedundefined;undefinedundefined// comma-separatedundefinedundefinedundefinedundefinedundefinedundefinednewundefinedundefinedFunction(undefinedundefined'a , b'undefinedundefined,undefinedundefined'return a + b')undefinedundefined;undefinedundefined// comma-separated with spacesundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Functions created with undefinedundefinednew Function, have
undefinedundefined[[Environment]] referencing the global Lexical Environment, not the outer one. Hence,
they cannot use outer variables. But that's actually good, because it insures us from errors. Passing parameters
explicitly is a much better method architecturally and causes no problems with minifiers.undefinedundefined
We may decide to execute a function not right now, but at a certain time later. That's called "scheduling a call".
undefinedundefinedThere are two methods for it:
undefinedundefinedsetTimeout allows us to run a function once after the interval of
time.undefinedundefinedsetInterval allows us to run a
function repeatedly, starting after the interval of time, then repeating continuously at that
interval.undefinedundefinedThese methods are not a part of JavaScript specification. But most environments have the internal scheduler and provide these methods. In particular, they are supported in all browsers and Node.js.
undefinedundefinedThe syntax:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet timerId undefinedundefined=undefinedundefinedsetTimeout(funcundefinedundefined|codeundefinedundefined, [delay]undefinedundefined, [arg1]undefinedundefined, [arg2]undefinedundefined, ...)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Parameters:
undefinedundefinedfunc|codeundefinedundefineddelayundefinedundefinedarg1, undefinedundefinedarg2…undefinedundefined
For instance, this code calls undefinedundefinedsayHi()
after one second:undefinedundefined
run function sayHi() { alert(‘Hello'); }
undefinedundefinedundefinedundefined! setTimeout(sayHi, 1000); undefinedundefined/! undefinedundefined
undefinedundefinedWith arguments:
undefinedundefinedrun function sayHi(phrase, who) { alert( phrase + ‘,''' + who ); }
undefinedundefinedundefinedundefined! setTimeout(sayHi, 1000, "Hello", "John"); // Hello, John undefinedundefined/! undefinedundefined
undefinedundefinedIf the first argument is a string, then JavaScript creates a function from it.
undefinedundefinedSo, this will also work:
undefinedundefined
undefinedundefinedjs run no-beautify setTimeout("alert('Hello')", 1000);undefinedundefined
But using strings is not recommended, use arrow functions instead of them, like this:
undefinedundefined
undefinedundefinedjs run no-beautify setTimeout(() => alert('Hello'), 1000);undefinedundefined
undefinedundefinedsmart header="Pass a function, but don't run it" Novice developers sometimes make a mistake by adding brackets()`
after the function:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// wrong!undefinedundefinedundefinedundefinedundefinedundefinedsetTimeout(undefinedundefinedsayHi()undefinedundefined,undefinedundefined1000)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That doesn't work, because undefinedundefinedsetTimeout expects a reference to a
function. And here undefinedundefinedsayHi() runs the function, and the undefinedundefinedresult of
its execution is passed to undefinedundefinedsetTimeout. In our case the result of
undefinedundefinedsayHi() is undefinedundefinedundefined (the function returns nothing), so
nothing is scheduled.
undefinedundefined
A call to undefinedundefinedsetTimeout returns a "timer identifier"
undefinedundefinedtimerId that we can use to cancel the execution.undefinedundefined
The syntax to cancel:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet timerId undefinedundefined=undefinedundefinedsetTimeout(...)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedclearTimeout(timerId)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In the code below, we schedule the function and then cancel it (changed our mind). As a result, nothing happens:
undefinedundefinedrun no-beautify let timerId = setTimeout(() => alert("never happens"), 1000); alert(timerId); // timer identifier
undefinedundefinedclearTimeout(timerId); alert(timerId); // same identifier (doesn't become null after canceling)
undefinedundefinedAs we can see from undefinedundefinedalert output, in a browser the timer
identifier is a number. In other environments, this can be something else. For instance, Node.js returns a timer
object with additional methods.undefinedundefined
Again, there is no universal specification for these methods, so that's fine.
undefinedundefinedFor browsers, timers are described in the undefinedundefinedtimers section of HTML5 standard.undefinedundefined
undefinedundefinedThe
undefinedundefinedsetInterval method has the same syntax as
undefinedundefinedsetTimeout:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet timerId undefinedundefined=undefinedundefinedsetInterval(funcundefinedundefined|codeundefinedundefined, [delay]undefinedundefined, [arg1]undefinedundefined, [arg2]undefinedundefined, ...)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
All arguments have the same meaning. But unlike undefinedundefinedsetTimeout it
runs the function not only once, but regularly after the given interval of time.undefinedundefined
To stop further calls, we should call
undefinedundefinedclearInterval(timerId).undefinedundefined
The following example will show the message every 2 seconds. After 5 seconds, the output is stopped:
undefinedundefinedrun // repeat with the interval of 2 seconds let timerId = setInterval(() => alert(‘tick'), 2000);
undefinedundefined// after 5 seconds stop setTimeout(() => { clearInterval(timerId); alert(‘stop'); }, 5000);
undefinedundefined
``undefinedundefinedsmart header="Time goes on whilealertundefinedundefinedis shown" In most browsers, including Chrome and Firefox the internal timer continues "ticking" while showingalert/confirm/prompt`.undefinedundefined
So if you run the code above and don't dismiss the undefinedundefinedalert window
for some time, then the next undefinedundefinedalert will be shown immediately as you do it. The actual
interval between alerts will be shorter than 2 seconds.
undefinedundefined
There are two ways of running something regularly.
undefinedundefinedOne is
undefinedundefinedsetInterval. The other one is a nested undefinedundefinedsetTimeout, like
this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined/** instead of:undefinedundefinedundefinedundefinedundefinedundefinedlet timerId = setInterval(() => alert('tick'), 2000);undefinedundefinedundefinedundefinedundefinedundefined*/undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet timerId undefinedundefined=undefinedundefinedsetTimeout(undefinedundefinedfunctionundefinedundefinedtick() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined'tick')undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefined timerId undefinedundefined=undefinedundefinedsetTimeout(tickundefinedundefined,undefinedundefined2000)undefinedundefined;undefinedundefined// (*)undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined}, 2000undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The undefinedundefinedsetTimeout above schedules the next call right at the end
of the current one undefinedundefined(*).undefinedundefined
The nested
undefinedundefinedsetTimeout is a more flexible method than undefinedundefinedsetInterval.
This way the next call may be scheduled differently, depending on the results of the current one.undefinedundefined
For instance, we need to write a service that sends a request to the server every 5 seconds asking for data, but in case the server is overloaded, it should increase the interval to 10, 20, 40 seconds…
undefinedundefinedHere's the pseudocode:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet delay undefinedundefined=undefinedundefined5000undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet timerId undefinedundefined=undefinedundefinedsetTimeout(undefinedundefinedfunctionundefinedundefinedrequest() undefinedundefined{undefinedundefinedundefinedundefined ...undefinedundefinedsendundefinedundefinedrequest...undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedif (request failed due to server overload) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// increase the interval to the next runundefinedundefinedundefinedundefined delay undefinedundefined*=undefinedundefined2undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined timerId undefinedundefined=undefinedundefinedsetTimeout(requestundefinedundefined, delay)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined}, delay)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
And if the functions that we're scheduling are CPU-hungry, then we can measure the time taken by the execution and plan the next call sooner or later.
undefinedundefinedundefinedundefinedNested
undefinedundefinedsetTimeout allows to set the delay between the executions more precisely than
undefinedundefinedsetInterval.undefinedundefinedundefinedundefined
Let's compare two code fragments. The first one uses undefinedundefinedsetInterval:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet i undefinedundefined=undefinedundefined1undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedsetInterval(undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedfunc(iundefinedundefined++)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined},undefinedundefined100)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The second one uses nested undefinedundefinedsetTimeout:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet i undefinedundefined=undefinedundefined1undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedsetTimeout(undefinedundefinedfunctionundefinedundefinedrun() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedfunc(iundefinedundefined++)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedsetTimeout(runundefinedundefined,undefinedundefined100)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined},undefinedundefined100)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
For undefinedundefinedsetInterval the internal scheduler will run
undefinedundefinedfunc(i++) every 100ms:undefinedundefined
undefinedundefinedundefinedundefined
Did you notice?
undefinedundefined
undefinedundefinedThe real delay between undefinedundefinedfunc calls for
undefinedundefinedsetInterval is less than in the code!undefinedundefinedundefinedundefined
That's normal, because the time taken by undefinedundefinedfunc's execution
"consumes" a part of the interval.undefinedundefined
It is possible that
undefinedundefinedfunc's execution turns out to be longer than we expected and takes more than
100ms.undefinedundefined
In this case the engine waits for undefinedundefinedfunc
to complete, then checks the scheduler and if the time is up, runs it again
undefinedundefinedimmediately.undefinedundefined
In the edge case, if the function
always executes longer than undefinedundefineddelay ms, then the calls will happen without a pause at
all.undefinedundefined
And here is the picture for the nested
undefinedundefinedsetTimeout:undefinedundefined
undefinedundefinedundefinedundefined
undefinedundefinedThe nested
undefinedundefinedsetTimeout guarantees the fixed delay (here
100ms).undefinedundefinedundefinedundefined
That's because a new call is planned at the end of the previous one.
undefinedundefined
undefinedundefinedsmart header="Garbage collection and setInterval/setTimeout callback" When a function is passed insetInterval/setTimeout`,
an internal reference is created to it and saved in the scheduler. It prevents the function from being garbage
collected, even if there are no other references to it.undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// the function stays in memory until the scheduler calls itundefinedundefinedundefinedundefinedundefinedundefinedsetTimeout(undefinedundefinedfunction() undefinedundefined{...undefinedundefined},undefinedundefined100)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
For undefinedundefinedsetInterval the function stays in memory until
undefinedundefinedclearInterval is called.undefinedundefined
There's a side-effect. A function references the outer lexical environment, so, while it lives, outer variables live too. They may take much more memory than the function itself. So when we don't need the scheduled function anymore, it's better to cancel it, even if it's very small.
undefinedundefinedThere's a special
use case: undefinedundefinedsetTimeout(func, 0), or just
undefinedundefinedsetTimeout(func).undefinedundefined
This schedules the
execution of undefinedundefinedfunc as soon as possible. But the scheduler will invoke it only after the
currently executing script is complete.undefinedundefined
So the function is scheduled to run "right after" the current script.
undefinedundefinedFor instance, this outputs "Hello", then immediately "World":
undefinedundefinedrun setTimeout(() => alert("World"));
undefinedundefinedalert("Hello");
undefinedundefinedThe first line "puts the call into calendar after 0ms". But the scheduler will only "check the
calendar" after the current script is complete, so undefinedundefined"Hello" is first, and
undefinedundefined"World" - after it.undefinedundefined
There are also advanced browser-related use cases of zero-delay timeout, that we'll discuss in the chapter undefinedundefinedinfo:event-loop.undefinedundefined
undefinedundefinedsmart header="Zero delay is in fact not zero (in a browser)" In the browser, there's a limitation of how often nested timers can run. The undefinedundefinedHTML5 standard says: "after five nested timers, the interval is forced to be at least 4 milliseconds.".undefinedundefined
undefinedundefined
Let's demonstrate what it means with the example below. The undefinedundefinedsetTimeout call in it
re-schedules itself with zero delay. Each call remembers the real time from the previous one in the
undefinedundefinedtimes array. What do the real delays look like? Let's see:undefinedundefined
run let start = Date.now(); let times = [];
undefinedundefinedsetTimeout(function run() { times.push(Date.now() - start); // remember delay from the previous call
undefinedundefinedif (start + 100 < Date.now()) alert(times); // show the delays after 100ms else setTimeout(run); // else re-schedule });
undefinedundefined// an example of the output: // 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100
undefinedundefinedundefinedundefined
First timers run immediately (just as written in the spec), and then we see `9, 15, 20, 24...`. The 4+ ms obligatory delay between invocations comes into play.
The similar thing happens if we use `setInterval` instead of `setTimeout`: `setInterval(f)` runs `f` few times with zero-delay, and afterwards with 4+ ms delay.
That limitation comes from ancient times and many scripts rely on it, so it exists for historical reasons.
For server-side JavaScript, that limitation does not exist, and there exist other ways to schedule an immediate asynchronous job, like [setImmediate](https://nodejs.org/api/timers.html#timers_setimmediate_callback_args) for Node.js. So this note is browser-specific.undefinedundefined
undefinedundefinedsetTimeout(func, delay, ...args) and
undefinedundefinedsetInterval(func, delay, ...args) allow us to run the
undefinedundefinedfunc once/regularly after undefinedundefineddelay
milliseconds.undefinedundefinedclearTimeout/clearInterval with the value returned by
undefinedundefinedsetTimeout/setInterval.undefinedundefinedsetTimeout calls are a more flexible alternative to
undefinedundefinedsetInterval, allowing us to set the time undefinedundefinedbetween
executions more precisely.undefinedundefinedsetTimeout(func, 0) (the same as undefinedundefinedsetTimeout(func)) is
used to schedule the call "as soon as possible, but after the current script is complete".undefinedundefinedsetTimeout or for undefinedundefinedsetInterval (after 5th call) to 4ms.
That's for historical reasons.undefinedundefinedPlease note that all scheduling methods do not undefinedundefinedguarantee the exact delay.undefinedundefined
undefinedundefinedFor example, the in-browser timer may slow down for a lot of reasons: - The CPU is overloaded. - The browser tab is in the background mode. - The laptop is on battery.
undefinedundefinedAll that may increase the minimal timer resolution (the minimal delay) to 300ms or even 1000ms depending on the browser and OS-level performance settings.
undefinedundefinedJavaScript gives exceptional flexibility when dealing with functions. They can be passed around, used as objects, and now we'll see how to undefinedundefinedforward calls between them and undefinedundefineddecorate them.undefinedundefined
undefinedundefinedLet's say we have a function undefinedundefinedslow(x) which
is CPU-heavy, but its results are stable. In other words, for the same undefinedundefinedx it always
returns the same result.undefinedundefined
If the function is called often, we may want to cache (remember) the results to avoid spending extra-time on recalculations.
undefinedundefinedBut instead of
adding that functionality into undefinedundefinedslow() we'll create a wrapper function, that adds
caching. As we'll see, there are many benefits of doing so.undefinedundefined
Here's the code, and explanations follow:
undefinedundefined
``undefinedundefinedjs run function slow(x) { // there can be a heavy CPU-intensive job here alert(Called
with ${x}`); return x; }undefinedundefined
function cachingDecorator(func) { let cache = new Map();
undefinedundefinedreturn function(x) { if (cache.has(x)) { // if there's such key in cache return cache.get(x); // read the result from it }
undefinedundefinedundefinedundefinedlet result = func(x); // otherwise call func
cache.set(x, result); // and cache (remember) the result
return result;undefinedundefinedundefinedundefined}; }
undefinedundefinedslow = cachingDecorator(slow);
undefinedundefinedalert( slow(1) ); // slow(1) is cached and the result returned alert( "Again:" + slow(1) ); // slow(1) result returned from cache
undefinedundefinedalert( slow(2) ); // slow(2) is cached and the result returned alert( "Again:" + slow(2) ); // slow(2) result returned from cache
undefinedundefinedIn the code above undefinedundefinedcachingDecorator is a
undefinedundefineddecorator: a special function that takes another function and alters its
behavior.undefinedundefined
The idea is that we can call
undefinedundefinedcachingDecorator for any function, and it will return the caching wrapper. That's
great, because we can have many functions that could use such a feature, and all we need to do is to apply
undefinedundefinedcachingDecorator to them.undefinedundefined
By separating caching from the main function code we also keep the main code simpler.
undefinedundefinedThe result of
undefinedundefinedcachingDecorator(func) is a "wrapper": undefinedundefinedfunction(x) that
"wraps" the call of undefinedundefinedfunc(x) into caching logic:undefinedundefined
undefinedundefinedundefinedundefined
From an
outside code, the wrapped undefinedundefinedslow function still does the same. It just got a caching
aspect added to its behavior.undefinedundefined
To summarize, there are several benefits of
using a separate undefinedundefinedcachingDecorator instead of altering the code of
undefinedundefinedslow itself:undefinedundefined
cachingDecorator is reusable. We can apply it to another function.undefinedundefined
slow itself (if there was any).undefinedundefinedThe caching decorator mentioned above is not suited to work with object methods.
undefinedundefinedFor instance, in the code
below undefinedundefinedworker.slow() stops working after the decoration:undefinedundefined
run // we'll make worker.slow caching let worker = { someMethod() { return 1; },
undefinedundefinedslow(x) { // scary CPU-heavy task hereundefinedundefined
alert("Called with" + x); return x * this.someMethod(); // (*) } };undefinedundefined
// same code as before function cachingDecorator(func) { let cache = new Map(); return function(x) { if (cache.has(x)) { return cache.get(x); } undefinedundefined! let result = func(x); // (**) undefinedundefined/! cache.set(x, result); return result; }; }undefinedundefined
undefinedundefinedalert( worker.slow(1) ); // the original method works
undefinedundefinedworker.slow = cachingDecorator(worker.slow); // now make it caching
undefinedundefinedundefinedundefined! alert( worker.slow(2) ); // Whoops! Error: Cannot read property ‘someMethod' of undefined undefinedundefined/! undefinedundefined
undefinedundefinedThe error occurs in the line undefinedundefined(*) that tries
to access undefinedundefinedthis.someMethod and fails. Can you see why?undefinedundefined
The reason is that the wrapper calls the original function as
undefinedundefinedfunc(x) in the line undefinedundefined(**). And, when called like that,
the function gets undefinedundefinedthis = undefined.undefinedundefined
We would observe a similar symptom if we tried to run:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet func undefinedundefined=undefinedundefinedworker.undefinedundefinedslowundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedfunc(undefinedundefined2)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
So, the wrapper passes the call to the original method, but without the context
undefinedundefinedthis. Hence the error.undefinedundefined
Let's fix it.
undefinedundefinedThere's a special built-in function method undefinedundefinedfunc.call(context, …args) that allows to call a function explicitly setting
undefinedundefinedthis.undefinedundefined
The syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunc.undefinedundefinedcall(contextundefinedundefined, arg1undefinedundefined, arg2undefinedundefined, ...)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
It runs undefinedundefinedfunc providing the first argument as
undefinedundefinedthis, and the next as the arguments.undefinedundefined
To put it simply, these two calls do almost the same:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunc(undefinedundefined1undefinedundefined,undefinedundefined2undefinedundefined,undefinedundefined3)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedfunc.undefinedundefinedcall(objundefinedundefined,undefinedundefined1undefinedundefined,undefinedundefined2undefinedundefined,undefinedundefined3)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
They both call undefinedundefinedfunc with arguments
undefinedundefined1, undefinedundefined2 and undefinedundefined3. The only
difference is that undefinedundefinedfunc.call also sets undefinedundefinedthis to
undefinedundefinedobj.undefinedundefined
As an example, in the code below we call
undefinedundefinedsayHi in the context of different objects:
undefinedundefinedsayHi.call(user) runs undefinedundefinedsayHi providing
undefinedundefinedthis=user, and the next line sets
undefinedundefinedthis=admin:undefinedundefined
run function sayHi() { alert(this.name); }
undefinedundefinedlet user = { name: "John" }; let admin = { name: "Admin" };
undefinedundefined// use call to pass different objects as "this" sayHi.call( user ); // John sayHi.call( admin ); // Admin
undefinedundefinedAnd here we use undefinedundefinedcall to call undefinedundefinedsay
with the given context and phrase:undefinedundefined
run function say(phrase) { alert(this.name + ‘:''' + phrase); }
undefinedundefinedlet user = { name: "John" };
undefinedundefined// user becomes this, and "Hello" becomes the first argument say.call( user, "Hello" ); // John: Hello
undefinedundefinedIn our case, we can use undefinedundefinedcall in the wrapper to pass the context
to the original function:undefinedundefined
run let worker = { someMethod() { return 1; },
undefinedundefinedslow(x) { alert("Called with" + x); return x * this.someMethod(); // (*) } };
undefinedundefinedfunction cachingDecorator(func) { let cache = new Map(); return function(x) { if (cache.has(x)) { return cache.get(x); } undefinedundefined! let result = func.call(this, x); // "this" is passed correctly now undefinedundefined/! cache.set(x, result); return result; }; }undefinedundefined
undefinedundefinedworker.slow = cachingDecorator(worker.slow); // now make it caching
undefinedundefinedalert( worker.slow(2) ); // works alert( worker.slow(2) ); // works, doesn't call the original (cached)
undefinedundefinedNow everything is fine.
undefinedundefinedTo make it all clear, let's see more deeply how
undefinedundefinedthis is passed along:undefinedundefined
worker.slow is now the wrapper
undefinedundefinedfunction (x) { ... }.undefinedundefinedworker.slow(2) is executed, the wrapper gets undefinedundefined2 as an
argument and undefinedundefinedthis=worker (it's the object before dot).undefinedundefinedfunc.call(this, x) passes the current undefinedundefinedthis
(undefinedundefined=worker) and the current argument (undefinedundefined=2) to the
original method.undefinedundefinedNow let's make undefinedundefinedcachingDecorator even more
universal. Till now it was working only with single-argument functions.undefinedundefined
Now
how to cache the multi-argument undefinedundefinedworker.slow method?undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet worker undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedslow(minundefinedundefined, max) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturn min undefinedundefined+ maxundefinedundefined;undefinedundefined// scary CPU-hogger is assumedundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// should remember same-argument callsundefinedundefinedundefinedundefinedundefinedundefinedworker.undefinedundefinedslowundefinedundefined=undefinedundefinedcachingDecorator(undefinedundefinedworker.undefinedundefinedslow)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Previously, for a single argument undefinedundefinedx we could just
undefinedundefinedcache.set(x, result) to save the result and undefinedundefinedcache.get(x)
to retrieve it. But now we need to remember the result for a undefinedundefinedcombination of
argumentsundefinedundefined(min,max). The native undefinedundefinedMap takes single
value only as the key.undefinedundefined
There are many solutions possible:
undefinedundefinedcache.set(min) will be a undefinedundefinedMap that stores the pair
undefinedundefined(max, result). So we can get undefinedundefinedresult as
undefinedundefinedcache.get(min).get(max).undefinedundefined"min,max" as the
undefinedundefinedMap key. For flexibility, we can allow to provide a undefinedundefinedhashing
function for the decorator, that knows how to make one value from many.undefinedundefinedFor many practical applications, the 3rd variant is good enough, so we'll stick to it.
undefinedundefinedAlso we need to pass not just undefinedundefinedx, but all arguments
in undefinedundefinedfunc.call. Let's recall that in a undefinedundefinedfunction() we can
get a pseudo-array of its arguments as undefinedundefinedarguments, so
undefinedundefinedfunc.call(this, x) should be replaced with
undefinedundefinedfunc.call(this, ...arguments).undefinedundefined
Here's a more
powerful undefinedundefinedcachingDecorator:undefinedundefined
``undefinedundefinedjs run let worker = { slow(min, max) { alert(Called with
undefinedundefinedundefinedundefinedmundefinedundefinediundefinedundefinedn,undefinedundefined{max}`);
return min + max; } };undefinedundefined
function cachingDecorator(func, hash) { let cache = new Map(); return function() { undefinedundefined! let key = hash(arguments); // (undefinedundefined) /!* if (cache.has(key)) { return cache.get(key); }undefinedundefined
undefinedundefinedundefinedundefined! let result = func.call(this, …arguments); // (**) undefinedundefined/!undefinedundefined
undefinedundefinedundefinedundefinedcache.set(key, result);
return result;undefinedundefinedundefinedundefined}; }
undefinedundefinedfunction hash(args) { return args[0] + ‘,''' + args[1]; }
undefinedundefinedworker.slow = cachingDecorator(worker.slow, hash);
undefinedundefinedalert( worker.slow(3, 5) ); // works alert( "Again" + worker.slow(3, 5) ); // same (cached)
undefinedundefinedNow it works with any number of arguments (though the hash function would also need to be adjusted to allow any number of arguments. An interesting way to handle this will be covered below).
undefinedundefinedThere are two changes:
undefinedundefined(*) it calls undefinedundefinedhash to create a single key from
undefinedundefinedarguments. Here we use a simple "joining" function that turns arguments
undefinedundefined(3, 5) into the key undefinedundefined"3,5". More complex cases may
require other hashing functions.undefinedundefined(**) uses undefinedundefinedfunc.call(this, ...arguments) to pass both
the context and all arguments the wrapper got (not just the first one) to the original function.undefinedundefined
Instead of
undefinedundefinedfunc.call(this, ...arguments) we could use
undefinedundefinedfunc.apply(this, arguments).undefinedundefined
The syntax of built-in method undefinedundefinedfunc.apply is:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunc.undefinedundefinedapply(contextundefinedundefined, args)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
It runs the undefinedundefinedfunc setting
undefinedundefinedthis=context and using an array-like object undefinedundefinedargs as the
list of arguments.undefinedundefined
The only syntax difference between
undefinedundefinedcall and undefinedundefinedapply is that
undefinedundefinedcall expects a list of arguments, while undefinedundefinedapply takes an
array-like object with them.undefinedundefined
So these two calls are almost equivalent:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunc.undefinedundefinedcall(contextundefinedundefined, ...undefinedundefinedargs)undefinedundefined;undefinedundefined// pass an array as list with spread syntaxundefinedundefinedundefinedundefinedundefinedundefinedfunc.undefinedundefinedapply(contextundefinedundefined, args)undefinedundefined;undefinedundefined// is same as using callundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
There's only a subtle difference:
undefinedundefined... allows to pass
undefinedundefinediterableundefinedundefinedargs as the list to
undefinedundefinedcall.undefinedundefinedapply accepts only
undefinedundefinedarray-likeundefinedundefinedargs.undefinedundefinedSo, where we expect an iterable, undefinedundefinedcall works, and where we
expect an array-like, undefinedundefinedapply works.undefinedundefined
And for
objects that are both iterable and array-like, like a real array, we can use any of them, but
undefinedundefinedapply will probably be faster, because most JavaScript engines internally optimize it
better.undefinedundefined
Passing all arguments along with the context to another function is called undefinedundefinedcall forwarding.undefinedundefined
undefinedundefinedThat's the simplest form of it:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet wrapper undefinedundefined=undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedfunc.undefinedundefinedapply(undefinedundefinedthisundefinedundefined,undefinedundefinedarguments)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
When an external code calls such undefinedundefinedwrapper, it is
indistinguishable from the call of the original function undefinedundefinedfunc.undefinedundefined
Now let's make one more minor improvement in the hashing function:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedhash(args) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturn args[undefinedundefined0] undefinedundefined+undefinedundefined','undefinedundefined+ args[undefinedundefined1]undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
As of now, it works only on two arguments. It would be better if it could glue any number of
undefinedundefinedargs.undefinedundefined
The natural solution would be to use undefinedundefinedarr.join method:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedhash(args) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedargs.undefinedundefinedjoin()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…Unfortunately, that won't work. Because we are calling
undefinedundefinedhash(arguments), and undefinedundefinedarguments object is both iterable
and array-like, but not a real array.undefinedundefined
So calling
undefinedundefinedjoin on it would fail, as we can see below:undefinedundefined
run function hash() { undefinedundefined! alert( arguments.join() ); // Error: arguments.join is not a function undefinedundefined/! }undefinedundefined
undefinedundefinedhash(1, 2);
undefinedundefinedStill, there's an easy way to use array join:
undefinedundefinedrun function hash() { undefinedundefined! alert( [].join.call(arguments) ); // 1,2 undefinedundefined/! }undefinedundefined
undefinedundefinedhash(1, 2);
undefinedundefinedThe trick is called undefinedundefinedmethod borrowing.undefinedundefined
undefinedundefinedWe take (borrow) a join method from a regular array (undefinedundefined[].join) and
use undefinedundefined[].join.call to run it in the context of
undefinedundefinedarguments.undefinedundefined
Why does it work?
undefinedundefinedThat's because the internal algorithm of the native method
undefinedundefinedarr.join(glue) is very simple.undefinedundefined
Taken from the specification almost "as-is":
undefinedundefinedglue be the first argument or, if no arguments, then a comma
undefinedundefined",".undefinedundefinedresult be an empty string.undefinedundefinedthis[0] to undefinedundefinedresult.undefinedundefinedglue and
undefinedundefinedthis[1].undefinedundefinedglue and undefinedundefinedthis[2].undefinedundefinedthis.length items are glued.undefinedundefinedresult.undefinedundefinedSo, technically it takes undefinedundefinedthis and joins
undefinedundefinedthis[0], undefinedundefinedthis[1] …etc together. It's intentionally
written in a way that allows any array-like undefinedundefinedthis (not a coincidence, many methods
follow this practice). That's why it also works with undefinedundefinedthis=arguments.undefinedundefined
It is generally safe to replace a function or a method with a decorated one, except for one little
thing. If the original function had properties on it, like undefinedundefinedfunc.calledCount or
whatever, then the decorated one will not provide them. Because that is a wrapper. So one needs to be careful if one
uses them.undefinedundefined
E.g. in the example above if undefinedundefinedslow
function had any properties on it, then undefinedundefinedcachingDecorator(slow) is a wrapper without
them.undefinedundefined
Some decorators may provide their own properties. E.g. a decorator may count how many times a function was invoked and how much time it took, and expose this information via wrapper properties.
undefinedundefinedThere exists a way to create decorators that keep access to function properties,
but this requires using a special undefinedundefinedProxy object to wrap a function. We'll discuss it
later in the article undefinedundefinedinfo:proxy#proxy-apply.undefinedundefined
undefinedundefinedDecorator is a wrapper around a function that alters its behavior. The main job is still carried out by the function.undefinedundefined
undefinedundefinedDecorators can be seen as "features" or "aspects" that can be added to a function. We can add one or add many. And all this without changing its code!
undefinedundefinedTo implement undefinedundefinedcachingDecorator, we studied
methods:undefinedundefined
func with
given context and arguments.undefinedundefinedfunc passing
undefinedundefinedcontext as undefinedundefinedthis and array-like
undefinedundefinedargs into a list of arguments.undefinedundefinedThe generic undefinedundefinedcall forwarding is usually done with
undefinedundefinedapply:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet wrapper undefinedundefined=undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedoriginal.undefinedundefinedapply(undefinedundefinedthisundefinedundefined,undefinedundefinedarguments)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
We also saw an example of undefinedundefinedmethod borrowing when we take a method
from an object and undefinedundefinedcall it in the context of another object. It is quite common to take
array methods and apply them to undefinedundefinedarguments. The alternative is to use rest parameters
object that is a real array.undefinedundefined
There are many decorators there in the wild. Check how well you got them by solving the tasks of this chapter.
undefinedundefinedlibs: - lodash
undefinedundefinedWhen passing object methods
as callbacks, for instance to undefinedundefinedsetTimeout, there's a known problem: "losing
undefinedundefinedthis".undefinedundefined
In this chapter we'll see the ways to fix it.
undefinedundefinedWe've already seen examples
of losing undefinedundefinedthis. Once a method is passed somewhere separately from the object -
undefinedundefinedthis is lost.undefinedundefined
Here's how it may happen with
undefinedundefinedsetTimeout:undefinedundefined
``undefinedundefinedjs run let user = { firstName: "John", sayHi() { alert(Hello,
${this.firstName}!`); } };undefinedundefined
undefinedundefined! setTimeout(user.sayHi, 1000); // Hello, undefined! undefinedundefined/! undefinedundefined
undefinedundefinedAs we can see, the output shows not "John" as
undefinedundefinedthis.firstName, but undefinedundefinedundefined!undefinedundefined
That's because undefinedundefinedsetTimeout got the function
undefinedundefineduser.sayHi, separately from the object. The last line can be rewritten
as:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet f undefinedundefined=undefinedundefineduser.undefinedundefinedsayHiundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedsetTimeout(fundefinedundefined,undefinedundefined1000)undefinedundefined;undefinedundefined// lost user contextundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The method undefinedundefinedsetTimeout in-browser is a little special: it sets
undefinedundefinedthis=window for the function call (for Node.js, undefinedundefinedthis
becomes the timer object, but doesn't really matter here). So for undefinedundefinedthis.firstName it
tries to get undefinedundefinedwindow.firstName, which does not exist. In other similar cases, usually
undefinedundefinedthis just becomes undefinedundefinedundefined.undefinedundefined
The task is quite typical - we want to pass an object method somewhere else (here - to the scheduler) where it will be called. How to make sure that it will be called in the right context?
undefinedundefinedThe simplest solution is to use a wrapping function:
undefinedundefined
``undefinedundefinedjs run let user = { firstName: "John", sayHi() { alert(Hello,
${this.firstName}!`); } };undefinedundefined
undefinedundefined! setTimeout(function() { user.sayHi(); // Hello, John! }, 1000); undefinedundefined/! undefinedundefined
undefinedundefinedNow it works, because it receives undefinedundefineduser from
the outer lexical environment, and then calls the method normally.undefinedundefined
The same, but shorter:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedsetTimeout(() undefinedundefined=>undefinedundefineduser.undefinedundefinedsayHi()undefinedundefined,undefinedundefined1000)undefinedundefined;undefinedundefined// Hello, John!undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Looks fine, but a slight vulnerability appears in our code structure.
undefinedundefinedWhat if before undefinedundefinedsetTimeout triggers (there's one second delay!)
undefinedundefineduser changes value? Then, suddenly, it will call the wrong object!undefinedundefined
``undefinedundefinedjs run let user = { firstName: "John", sayHi() { alert(Hello,
${this.firstName}!`); } };undefinedundefined
setTimeout(() => user.sayHi(), 1000);
undefinedundefined// …the value of user changes within 1 second user = { sayHi() { alert("Another user in setTimeout!"); } };
undefinedundefined// Another user in setTimeout!
undefinedundefinedThe next solution guarantees that such thing won't happen.
undefinedundefinedFunctions provide a built-in method
undefinedundefinedbind that allows to fix
undefinedundefinedthis.undefinedundefined
The basic syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// more complex syntax will come a little laterundefinedundefinedundefinedundefinedundefinedundefinedlet boundFunc undefinedundefined=undefinedundefinedfunc.undefinedundefinedbind(context)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The result of undefinedundefinedfunc.bind(context) is a special function-like
"exotic object", that is callable as function and transparently passes the call to undefinedundefinedfunc
setting undefinedundefinedthis=context.undefinedundefined
In other words, calling
undefinedundefinedboundFunc is like undefinedundefinedfunc with fixed
undefinedundefinedthis.undefinedundefined
For instance, here
undefinedundefinedfuncUser passes a call to undefinedundefinedfunc with
undefinedundefinedthis=user:undefinedundefined
runundefinedundefined
let user = { firstName: "John" };undefinedundefined
function func() { alert(this.firstName); }
undefinedundefinedundefinedundefined! let funcUser = func.bind(user); funcUser(); //
Johnundefinedundefined
undefinedundefined/!
undefinedundefined
Here undefinedundefinedfunc.bind(user) as a "bound variant" of
undefinedundefinedfunc, with fixed undefinedundefinedthis=user.undefinedundefined
All arguments are passed to the original undefinedundefinedfunc "as is", for
instance:undefinedundefined
runundefinedundefined
let user = { firstName: "John" };undefinedundefined
function func(phrase) { alert(phrase + ‘,''' + this.firstName); }
undefinedundefined// bind this to user let funcUser = func.bind(user);
undefinedundefinedundefinedundefined! funcUser("Hello"); // Hello, John (argument "Hello" is passed, and this=user) undefinedundefined/! undefinedundefined
undefinedundefinedNow let's try with an object method:
undefinedundefined
``undefinedundefinedjs run let user = { firstName: "John", sayHi() { alert(Hello,
${this.firstName}!`); } };undefinedundefined
undefinedundefined! let sayHi = user.sayHi.bind(user); // (undefinedundefined) /!*undefinedundefined
undefinedundefined// can run it without an object sayHi(); // Hello, John!
undefinedundefinedsetTimeout(sayHi, 1000); // Hello, John!
undefinedundefined// even if the value of user changes within 1 second // sayHi uses the pre-bound value which is reference to the old user object user = { sayHi() { alert("Another user in setTimeout!"); } };
undefinedundefinedIn the line undefinedundefined(*) we take the method
undefinedundefineduser.sayHi and bind it to undefinedundefineduser. The
undefinedundefinedsayHi is a "bound" function, that can be called alone or passed to
undefinedundefinedsetTimeout - doesn't matter, the context will be right.undefinedundefined
Here we can see that arguments are passed "as is", only undefinedundefinedthis is
fixed by undefinedundefinedbind:undefinedundefined
``undefinedundefinedjs run let user = { firstName: "John", say(phrase) { alert(${phrase},
${this.firstName}!`); } };undefinedundefined
let say = user.say.bind(user);
undefinedundefinedsay("Hello"); // Hello, John ("Hello" argument is passed to say) say("Bye"); // Bye, John ("Bye" is passed to say)
undefinedundefined
undefinedundefinedsmart header="Convenience method:bindAll`" If an object has many methods and we plan to
actively pass it around, then we could bind them all in a loop:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedlet key undefinedundefinedin user) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (undefinedundefinedtypeof user[key] undefinedundefined==undefinedundefined'function') undefinedundefined{undefinedundefinedundefinedundefined user[key] undefinedundefined= user[key].undefinedundefinedbind(user)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
JavaScript libraries also provide functions for convenient mass binding , e.g. undefinedundefined_.bindAll(object, methodNames) in lodash. undefinedundefined
undefinedundefinedUntil
now we have only been talking about binding undefinedundefinedthis. Let's take it a step
further.undefinedundefined
We can bind not only undefinedundefinedthis, but also
arguments. That's rarely done, but sometimes can be handy.undefinedundefined
The full syntax
of undefinedundefinedbind:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet bound undefinedundefined=undefinedundefinedfunc.undefinedundefinedbind(contextundefinedundefined, [arg1]undefinedundefined, [arg2]undefinedundefined, ...)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
It allows to bind context as undefinedundefinedthis and starting arguments of
the function.undefinedundefined
For instance, we have a multiplication function
undefinedundefinedmul(a, b):undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedmul(aundefinedundefined, b) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturn a undefinedundefined* bundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Let's use undefinedundefinedbind to create a function
undefinedundefineddouble on its base:undefinedundefined
run function mul(a, b) { return a * b; }
undefinedundefinedundefinedundefined! let double = mul.bind(null, 2); undefinedundefined/!undefinedundefined
undefinedundefinedalert( double(3) ); // = mul(2, 3) = 6 alert( double(4) ); // = mul(2, 4) = 8 alert( double(5) ); // = mul(2, 5) = 10
undefinedundefinedThe call to undefinedundefinedmul.bind(null, 2) creates a new function
undefinedundefineddouble that passes calls to undefinedundefinedmul, fixing
undefinedundefinednull as the context and undefinedundefined2 as the first argument. Further
arguments are passed "as is".undefinedundefined
That's called undefinedundefinedpartial function application - we create a new function by fixing some parameters of the existing one.undefinedundefined
undefinedundefinedPlease note that we actually
don't use undefinedundefinedthis here. But undefinedundefinedbind requires it, so we must
put in something like undefinedundefinednull.undefinedundefined
The function
undefinedundefinedtriple in the code below triples the value:undefinedundefined
run function mul(a, b) { return a * b; }
undefinedundefinedundefinedundefined! let triple = mul.bind(null, 3); undefinedundefined/!undefinedundefined
undefinedundefinedalert( triple(3) ); // = mul(3, 3) = 9 alert( triple(4) ); // = mul(3, 4) = 12 alert( triple(5) ); // = mul(3, 5) = 15
undefinedundefinedWhy do we usually make a partial function?
undefinedundefinedThe benefit is that we can
create an independent function with a readable name (undefinedundefineddouble,
undefinedundefinedtriple). We can use it and not provide the first argument every time as it's fixed with
undefinedundefinedbind.undefinedundefined
In other cases, partial application is useful when we have a very generic function and want a less universal variant of it for convenience.
undefinedundefinedFor instance, we have a function undefinedundefinedsend(from, to, text). Then, inside
a undefinedundefineduser object we may want to use a partial variant of it:
undefinedundefinedsendTo(to, text) that sends from the current user.undefinedundefined
What if
we'd like to fix some arguments, but not the context undefinedundefinedthis? For example, for an object
method.undefinedundefined
The native undefinedundefinedbind does not allow that.
We can't just omit the context and jump to arguments.undefinedundefined
Fortunately, a
function undefinedundefinedpartial for binding only arguments can be easily
implemented.undefinedundefined
Like this:
undefinedundefinedrun undefinedundefined! function partial(func, …argsBound) { return function(…args) { // (undefinedundefined) return func.call(this, …argsBound, …args); } } /!*undefinedundefined
undefinedundefined// Usage: let user
= { firstName: "John", say(time, phrase) {
alert(undefinedundefined[${time}] ${this.firstName}: ${phrase}!); } };undefinedundefined
// add a partial method with fixed time user.sayNow = partial(user.say, new Date().getHours() + ‘:''' + new Date().getMinutes());
undefinedundefineduser.sayNow("Hello"); // Something like: // [10:00] John: Hello!
undefinedundefinedThe result of undefinedundefinedpartial(func[, arg1, arg2...]) call is a wrapper
undefinedundefined(*) that calls undefinedundefinedfunc with: - Same
undefinedundefinedthis as it gets (for undefinedundefineduser.sayNow call it's
undefinedundefineduser) - Then gives it undefinedundefined...argsBound - arguments from the
undefinedundefinedpartial call (undefinedundefined"10:00") - Then gives it
undefinedundefined...args - arguments given to the wrapper
(undefinedundefined"Hello")undefinedundefined
So easy to do it with the spread syntax, right?
undefinedundefinedAlso there's a ready undefinedundefined_.partial implementation from lodash library.undefinedundefined
undefinedundefinedMethod
undefinedundefinedfunc.bind(context, ...args) returns a "bound variant" of function
undefinedundefinedfunc that fixes the context undefinedundefinedthis and first arguments if
given.undefinedundefined
Usually we apply undefinedundefinedbind to fix
undefinedundefinedthis for an object method, so that we can pass it somewhere. For example, to
undefinedundefinedsetTimeout.undefinedundefined
When we fix some arguments of an existing function, the resulting (less universal) function is called undefinedundefinedpartially applied or undefinedundefinedpartial.undefinedundefined
undefinedundefinedPartials are convenient when we don't
want to repeat the same argument over and over again. Like if we have a undefinedundefinedsend(from, to)
function, and undefinedundefinedfrom should always be the same for our task, we can get a partial and go
on with it.undefinedundefined
Let's revisit arrow functions.
undefinedundefinedArrow functions are not just a "shorthand" for writing small stuff. They have some very specific and useful features.
undefinedundefinedJavaScript is full of situations where we need to write a small function that's executed somewhere else.
undefinedundefinedFor instance:
undefinedundefinedarr.forEach(func) -
undefinedundefinedfunc is executed by undefinedundefinedforEach for every array
item.undefinedundefinedsetTimeout(func) -
undefinedundefinedfunc is executed by the built-in scheduler.undefinedundefinedIt's in the very spirit of JavaScript to create a function and pass it somewhere.
undefinedundefinedAnd in such functions we usually don't want to leave the current context. That's where arrow functions come in handy.
undefinedundefinedAs we remember from the
chapter undefinedundefinedinfo:object-methods, arrow functions do not
have undefinedundefinedthis. If undefinedundefinedthis is accessed, it is taken from the
outside.undefinedundefined
For instance, we can use it to iterate inside an object method:
undefinedundefinedrun let group = { title: "Our Group", students: ["John", "Pete", "Alice"],
undefinedundefinedshowList() { undefinedundefined! this.students.forEach( student => alert(this.title + ‘:''' + student) ); undefinedundefined/! } };undefinedundefined
undefinedundefinedgroup.showList();
undefinedundefinedHere in undefinedundefinedforEach, the arrow function is used, so
undefinedundefinedthis.title in it is exactly the same as in the outer method
undefinedundefinedshowList. That is: undefinedundefinedgroup.title.undefinedundefined
If we used a "regular" function, there would be an error:
undefinedundefinedrun let group = { title: "Our Group", students: ["John", "Pete", "Alice"],
undefinedundefinedshowList() { undefinedundefined! this.students.forEach(function(student) { // Error: Cannot read property ‘title' of undefined alert(this.title + ‘:''' + student); }); undefinedundefined/! } };undefinedundefined
undefinedundefinedgroup.showList();
undefinedundefinedThe error occurs because undefinedundefinedforEach runs functions with
undefinedundefinedthis=undefined by default, so the attempt to access
undefinedundefinedundefined.title is made.undefinedundefined
That doesn't affect
arrow functions, because they just don't have undefinedundefinedthis.undefinedundefined
undefinedundefinedwarn header="Arrow functions can't run with `new`" Not having `this` naturally means another limitation: arrow functions can't be used as constructors. They can't be called with `new`.undefinedundefined
``undefinedundefinedsmart header="Arrow functions VS bind" There's a subtle difference between an arrow function=>undefinedundefinedand a regular function called with.bind(this)`:undefinedundefined
.bind(this) creates a "bound version" of
the function.undefinedundefined=> doesn't
create any binding. The function simply doesn't have undefinedundefinedthis. The lookup of
undefinedundefinedthis is made exactly the same way as a regular variable search: in the outer lexical
environment.
undefinedundefinedArrow functions also have no undefinedundefinedarguments
variable.undefinedundefined
That's great for decorators, when we need to forward a call with
the current undefinedundefinedthis and undefinedundefinedarguments.undefinedundefined
For instance, undefinedundefineddefer(f, ms) gets a function and returns a wrapper
around it that delays the call by undefinedundefinedms milliseconds:undefinedundefined
run function defer(f, ms) { return function() { setTimeout(() => f.apply(this, arguments), ms); }; }
undefinedundefinedfunction sayHi(who) { alert(‘Hello,''' + who); }
undefinedundefinedlet sayHiDeferred = defer(sayHi, 2000); sayHiDeferred("John"); // Hello, John after 2 seconds
undefinedundefinedThe same without an arrow function would look like:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefineddefer(fundefinedundefined, ms) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedfunction(...undefinedundefinedargs) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet ctx undefinedundefined=undefinedundefinedthisundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedsetTimeout(undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedf.undefinedundefinedapply(ctxundefinedundefined, args)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}, ms)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Here we had to create additional variables undefinedundefinedargs and
undefinedundefinedctx so that the function inside undefinedundefinedsetTimeout could take
them.undefinedundefined
Arrow functions:
undefinedundefinedthisundefinedundefinedargumentsundefinedundefinednewundefinedundefinedsuper, but we didn't study it yet. We will on the chapter
undefinedundefinedinfo:class-inheritanceundefinedundefinedThat's because they are meant for short pieces of code that do not have their own "context", but rather work in the current one. And they really shine in that use case.
undefinedundefinedThe first thing we'll study is the building blocks of code.
undefinedundefinedStatements are syntax constructs and commands that perform actions.
undefinedundefinedWe've already seen a statement,
undefinedundefinedalert('Hello, world!'), which shows the message "Hello, world!".undefinedundefined
We can have as many statements in our code as we want. Statements can be separated with a semicolon.
undefinedundefinedFor example, here we split "Hello World" into two alerts:
undefinedundefined
undefinedundefinedjs run no-beautify alert('Hello'); alert('World');undefinedundefined
Usually, statements are written on separate lines to make the code more readable:
undefinedundefinedundefinedundefinedjs run no-beautify alert('Hello'); alert('World');undefinedundefined
A semicolon may be omitted in most cases when a line break exists.
undefinedundefinedThis would also work:
undefinedundefined
undefinedundefinedjs run no-beautify alert('Hello') alert('World')undefinedundefined
Here, JavaScript interprets the line break as an "implicit" semicolon. This is called an undefinedundefinedautomatic semicolon insertion.undefinedundefined
undefinedundefinedundefinedundefinedIn most cases, a newline implies a semicolon. But "in most cases" does not mean "always"!undefinedundefined
undefinedundefinedThere are cases when a newline does not mean a semicolon. For example:
undefinedundefined
undefinedundefinedjs run no-beautify alert(3 + 1 + 2);undefinedundefined
The code
outputs undefinedundefined6 because JavaScript does not insert semicolons here. It is intuitively obvious
that if the line ends with a plus undefinedundefined"+", then it is an "incomplete expression", so the
semicolon is not required. And in this case that works as intended.undefinedundefined
undefinedundefinedBut there are situations where JavaScript "fails" to assume a semicolon where it is really needed.undefinedundefined
undefinedundefinedErrors which occur in such cases are quite hard to find and fix.
undefinedundefinedsmart header="An example of an error" If you're curious to see a concrete example of such an error, check this code out:
undefinedundefinedundefinedundefinedjs run [1, 2].forEach(alert)undefinedundefined
No need to think about the meaning of the brackets undefinedundefined[] and
undefinedundefinedforEach yet. We'll study them later. For now, just remember the result of the code: it
shows undefinedundefined1 then undefinedundefined2.undefinedundefined
Now, let's add an undefinedundefinedalert before the code and undefinedundefinednot finish it
with a semicolon:undefinedundefined
run no-beautify alert("There will be an error")
undefinedundefined[1, 2].forEach(alert)
undefinedundefinedNow if we run the code, only the first undefinedundefinedalert is shown and then
we have an error!undefinedundefined
But everything is fine again if we add a semicolon after
undefinedundefinedalert: run alert("All fine now");undefinedundefined
[1,
2].forEach(alert)undefinedundefined
undefinedundefined
Now we have the "All fine now" message followed by
undefinedundefined1 and undefinedundefined2.undefinedundefined
The
error in the no-semicolon variant occurs because JavaScript does not assume a semicolon before square brackets
undefinedundefined[...].undefinedundefined
So, because the semicolon is not auto-inserted, the code in the first example is treated as a single statement. Here's how the engine sees it:
undefinedundefined
undefinedundefinedjs run no-beautify alert("There will be an error")[1, 2].forEach(alert)undefinedundefined
But it should be two separate statements, not one. Such a merging in this case is just wrong, hence the error. This can happen in other situations.
undefinedundefinedWe recommend putting semicolons between statements even if they are separated by newlines. This rule is widely adopted by the community. Let's note once again - undefinedundefinedit is possible to leave out semicolons most of the time. But it's safer - especially for a beginner - to use them.undefinedundefined
undefinedundefinedAs time goes on, programs become more and more complex. It becomes necessary to add undefinedundefinedcomments which describe what the code does and why.undefinedundefined
undefinedundefinedComments can be put into any place of a script. They don't affect its execution because the engine simply ignores them.
undefinedundefined
undefinedundefinedOne-line comments start with two forward slash characters
undefinedundefined//.undefinedundefinedundefinedundefined
The rest of the line is a comment. It may occupy a full line of its own or follow a statement.
undefinedundefinedLike here: run // This comment occupies a line of its own alert(‘Hello');
undefinedundefinedalert(‘World'); // This comment follows the statement
undefinedundefinedundefinedundefinedMultiline comments start with a forward slash and an asterisk
undefinedundefined/* and end with an asterisk and a forward slash
undefinedundefined*/.undefinedundefinedundefinedundefined
Like this:
undefinedundefined
undefinedundefinedjs run /* An example with two messages. This is a multiline comment. */ alert('Hello'); alert('World');undefinedundefined
The content of comments is ignored, so if we put code inside
undefinedundefined/* … */, it won't execute.undefinedundefined
Sometimes it can be handy to temporarily disable a part of code:
undefinedundefined
undefinedundefinedjs run /* Commenting out the code alert('Hello'); */ alert('World');undefinedundefined
undefinedundefinedsmart header="Use hotkeys!" In most editors, a line of code can be commented out by pressing the `key:Ctrl+/` hotkey for a single-line comment and something like `key:Ctrl+Shift+/` -- for multiline comments (select a piece of code and press the hotkey). For Mac, try `key:Cmd` instead of `key:Ctrl` and `key:Option` instead of `key:Shift`.undefinedundefined
undefinedundefinedwarn header="Nested comments are not supported!" There may not be/undefinedundefined…/undefinedundefinedinside another/undefinedundefined…/`.undefinedundefined
Such code will die with an error:
undefinedundefined
undefinedundefinedjs run no-beautify /* /* nested comment ?!? */ */ alert( 'World' );
undefinedundefined
Please, don't hesitate to comment your code.
undefinedundefinedComments increase the overall code footprint, but that's not a problem at all. There are many tools which minify code before publishing to a production server. They remove comments, so they don't appear in the working scripts. Therefore, comments do not have negative effects on production at all.
undefinedundefinedLater in the tutorial there will be a chapter undefinedundefinedinfo:code-quality that also explains how to write better comments.undefinedundefined
undefinedundefinedAs we know, objects can store properties.
undefinedundefinedUntil now, a property was a simple "key-value" pair to us. But an object property is actually a more flexible and powerful thing.
undefinedundefinedIn this chapter we'll study additional configuration options, and in the next we'll see how to invisibly turn them into getter/setter functions.
undefinedundefinedObject properties, besides a
undefinedundefinedundefinedundefinedvalueundefinedundefined, have three special
attributes (so-called "flags"):undefinedundefined
writableundefinedundefined - if
undefinedundefinedtrue, the value can be changed, otherwise it's read-only.undefinedundefinedenumerableundefinedundefined -
if undefinedundefinedtrue, then listed in loops, otherwise not listed.undefinedundefinedconfigurableundefinedundefined
- if undefinedundefinedtrue, the property can be deleted and these attributes can be modified,
otherwise not.undefinedundefinedWe didn't see them yet, because
generally they do not show up. When we create a property "the usual way", all of them are
undefinedundefinedtrue. But we also can change them anytime.undefinedundefined
First, let's see how to get those flags.
undefinedundefinedThe method undefinedundefinedObject.getOwnPropertyDescriptor allows to query the undefinedundefinedfull information about a property.undefinedundefined
undefinedundefinedThe syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet descriptor undefinedundefined=undefinedundefinedObject.undefinedundefinedgetOwnPropertyDescriptor(objundefinedundefined, propertyName)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
objundefinedundefinedpropertyNameundefinedundefinedThe returned value is a so-called "property descriptor" object: it contains the value and all the flags.
undefinedundefinedFor instance:
undefinedundefinedrun let user = { name: "John" };
undefinedundefinedlet descriptor = Object.getOwnPropertyDescriptor(user, ‘name');
undefinedundefinedalert( JSON.stringify(descriptor, null, 2 ) ); /* property descriptor: { "value": "John", "writable": true, "enumerable": true, "configurable": true } */
undefinedundefinedTo change the flags, we can use undefinedundefinedObject.defineProperty.undefinedundefined
undefinedundefinedThe syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedObject.undefinedundefineddefineProperty(objundefinedundefined, propertyNameundefinedundefined, descriptor)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
obj,
undefinedundefinedpropertyNameundefinedundefineddescriptorundefinedundefinedIf the property exists, undefinedundefineddefineProperty
updates its flags. Otherwise, it creates the property with the given value and flags; in that case, if a flag is not
supplied, it is assumed undefinedundefinedfalse.undefinedundefined
For instance,
here a property undefinedundefinedname is created with all falsy flags:undefinedundefined
run let user = {};
undefinedundefinedundefinedundefined! Object.defineProperty(user, "name", { value: "John" }); undefinedundefined/!undefinedundefined
undefinedundefinedlet descriptor = Object.getOwnPropertyDescriptor(user, ‘name');
undefinedundefinedalert( JSON.stringify(descriptor, null, 2 ) ); /undefinedundefined { "value": "John", !undefinedundefined "writable": false, "enumerable": false, "configurable": false /!undefinedundefined } / undefinedundefined
undefinedundefinedCompare it with "normally created" undefinedundefineduser.name
above: now all flags are falsy. If that's not what we want then we'd better set them to
undefinedundefinedtrue in undefinedundefineddescriptor.undefinedundefined
Now let's see effects of the flags by example.
undefinedundefinedLet's make undefinedundefineduser.name non-writable (can't be
reassigned) by changing undefinedundefinedwritable flag:undefinedundefined
run let user = { name: "John" };
undefinedundefinedObject.defineProperty(user, "name", { undefinedundefined! writable: false undefinedundefined/! });undefinedundefined
undefinedundefinedundefinedundefined! user.name = "Pete"; // Error: Cannot assign to read only property ‘name' undefinedundefined/! undefinedundefined
undefinedundefinedNow no one can change the name of our user, unless they apply their own
undefinedundefineddefineProperty to override ours.undefinedundefined
undefinedundefinedsmart header="Errors appear only in strict mode" In the non-strict mode, no errors occur when writing to non-writable properties and such. But the operation still won't succeed. Flag-violating actions are just silently ignored in non-strict.undefinedundefined
Here's the same example, but the property is created from scratch:
undefinedundefinedrun let user = { };
undefinedundefinedObject.defineProperty(user, "name", { undefinedundefined! value: "John", // for new properties we need to explicitly list what's true enumerable: true, configurable: true undefinedundefined/! });undefinedundefined
undefinedundefinedalert(user.name); // John user.name = "Pete"; // Error
undefinedundefinedNow let's add a custom
undefinedundefinedtoString to undefinedundefineduser.undefinedundefined
Normally, a built-in undefinedundefinedtoString for objects is non-enumerable, it does
not show up in undefinedundefinedfor..in. But if we add a undefinedundefinedtoString of our
own, then by default it shows up in undefinedundefinedfor..in, like this:undefinedundefined
run let user = { name: "John", toString() { return this.name; } };
undefinedundefined// By default, both our properties are listed: for (let key in user) alert(key); // name, toString
undefinedundefinedIf we don't like it, then we can set undefinedundefinedenumerable:false. Then it
won't appear in a undefinedundefinedfor..in loop, just like the built-in one:undefinedundefined
run let user = { name: "John", toString() { return this.name; } };
undefinedundefinedObject.defineProperty(user, "toString", { undefinedundefined! enumerable: false undefinedundefined/! });undefinedundefined
undefinedundefinedundefinedundefined! // Now our toString disappears: undefinedundefined/! for (let key in user) alert(key); // name undefinedundefined
undefinedundefinedNon-enumerable properties are also excluded from
undefinedundefinedObject.keys:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefinedObject.undefinedundefinedkeys(user))undefinedundefined;undefinedundefined// nameundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The non-configurable flag
(undefinedundefinedconfigurable:false) is sometimes preset for built-in objects and
properties.undefinedundefined
A non-configurable property can not be deleted.
undefinedundefinedFor instance, undefinedundefinedMath.PI is non-writable, non-enumerable and
non-configurable:undefinedundefined
run let descriptor = Object.getOwnPropertyDescriptor(Math, ‘PI');
undefinedundefinedalert( JSON.stringify(descriptor, null, 2 ) );
/undefinedundefined { "value": 3.141592653589793, "writable": false, "enumerable": false, "configurable": false }
/ ``undefinedundefinedSo, a programmer is unable to change the value ofMath.PI` or overwrite
it.undefinedundefined
run Math.PI = 3; // Error
undefinedundefined// delete Math.PI won't work either
undefinedundefinedMaking a property non-configurable is a one-way road. We cannot change it back with
undefinedundefineddefineProperty.undefinedundefined
To be precise,
non-configurability imposes several restrictions on undefinedundefineddefineProperty: 1. Can't change
undefinedundefinedconfigurable flag. 2. Can't change undefinedundefinedenumerable flag. 3.
Can't change undefinedundefinedwritable: false to undefinedundefinedtrue (the other way
round works). 4. Can't change undefinedundefinedget/set for an accessor property (but can assign them if
absent).undefinedundefined
undefinedundefinedThe idea of "configurable: false" is to prevent changes of property flags and its deletion, while allowing to change its value.undefinedundefined
undefinedundefinedHere undefinedundefineduser.name is non-configurable, but we can still change it
(as it's writable):undefinedundefined
run let user = { name: "John" };
undefinedundefinedObject.defineProperty(user, "name", { configurable: false });
undefinedundefineduser.name = "Pete"; // works fine delete user.name; // Error
undefinedundefinedAnd here we make undefinedundefineduser.name a "forever sealed"
constant:undefinedundefined
run let user = { name: "John" };
undefinedundefinedObject.defineProperty(user, "name", { writable: false, configurable: false });
undefinedundefined// won't be able to change user.name or its flags // all this won't work: user.name = "Pete"; delete user.name; Object.defineProperty(user, "name", { value: "Pete" });
undefinedundefinedThere's a method undefinedundefinedObject.defineProperties(obj, descriptors) that allows to define many properties at once.undefinedundefined
undefinedundefinedThe syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedObject.undefinedundefineddefineProperties(objundefinedundefined,undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedprop1undefinedundefined: descriptor1undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedprop2undefinedundefined: descriptor2undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
For instance:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedObject.undefinedundefineddefineProperties(userundefinedundefined,undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined{undefinedundefinedvalueundefinedundefined:undefinedundefined"John"undefinedundefined,undefinedundefinedwritableundefinedundefined:undefinedundefinedfalseundefinedundefined},undefinedundefinedundefinedundefinedundefinedundefinedsurnameundefinedundefined:undefinedundefined{undefinedundefinedvalueundefinedundefined:undefinedundefined"Smith"undefinedundefined,undefinedundefinedwritableundefinedundefined:undefinedundefinedfalseundefinedundefined},undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
So, we can set many properties at once.
undefinedundefinedTo get all property descriptors at once, we can use the method undefinedundefinedObject.getOwnPropertyDescriptors(obj).undefinedundefined
undefinedundefinedTogether with undefinedundefinedObject.defineProperties it can be used as a
"flags-aware" way of cloning an object:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet clone undefinedundefined=undefinedundefinedObject.undefinedundefineddefineProperties(undefinedundefined{},undefinedundefinedObject.undefinedundefinedgetOwnPropertyDescriptors(obj))undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Normally when we clone an object, we use an assignment to copy properties, like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedlet key undefinedundefinedin user) undefinedundefined{undefinedundefinedundefinedundefined clone[key] undefinedundefined= user[key]undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…But that does not copy flags. So if we want a "better" clone then
undefinedundefinedObject.defineProperties is preferred.undefinedundefined
Another
difference is that undefinedundefinedfor..in ignores symbolic properties, but
undefinedundefinedObject.getOwnPropertyDescriptors returns undefinedundefinedall property
descriptors including symbolic ones.undefinedundefined
Property descriptors work at the level of individual properties.
undefinedundefinedThere are also methods that limit access to the undefinedundefinedwhole object:undefinedundefined
undefinedundefinedconfigurable: false
for all existing properties.
undefinedundefinedconfigurable: false, writable: false for
all existing properties.
undefinedundefinedAnd also there are tests for them:
undefinedundefinedfalse if adding properties is forbidden, otherwise
undefinedundefinedtrue.
undefinedundefinedtrue if adding/removing properties is forbidden, and all existing properties have
undefinedundefinedconfigurable: false.
undefinedundefinedtrue if adding/removing/changing properties is forbidden, and all current properties
are undefinedundefinedconfigurable: false, writable: false.
undefinedundefinedThese methods are rarely used in practice.
undefinedundefinedThere are two kinds of object properties.
undefinedundefinedThe first kind is undefinedundefineddata properties. We already know how to work with them. All properties that we've been using until now were data properties.undefinedundefined
undefinedundefinedThe second type of properties is something new. It's undefinedundefinedaccessor properties. They are essentially functions that execute on getting and setting a value, but look like regular properties to an external code.undefinedundefined
undefinedundefinedAccessor properties are represented by "getter"
and "setter" methods. In an object literal they are denoted by undefinedundefinedget and
undefinedundefinedset:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet obj undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*get undefinedundefinedpropName()undefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined // getter, the code executed on getting obj.propNameundefinedundefinedundefinedundefinedundefinedundefined },undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined!undefinedundefined*undefinedundefinedset propNameundefinedundefined(undefinedundefinedvalueundefinedundefined)*undefinedundefined/undefinedundefined!*undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// setter, the code executed on setting obj.propName = valueundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The getter works when undefinedundefinedobj.propName is read, the setter - when
it is assigned.undefinedundefined
For instance, we have a undefinedundefineduser
object with undefinedundefinedname and undefinedundefinedsurname:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedsurnameundefinedundefined:undefinedundefined"Smith"undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Now we want to add a undefinedundefinedfullName property, that should be
undefinedundefined"John Smith". Of course, we don't want to copy-paste existing information, so we can
implement it as an accessor:undefinedundefined
run let user = { name: "John", surname: "Smith",
undefinedundefinedundefinedundefined! get fullName() { return
undefinedundefined${this.name} ${this.surname}; } undefinedundefined/! };undefinedundefined
undefinedundefined! alert(user.fullName); // John Smith undefinedundefined/! undefinedundefined
undefinedundefinedFrom the outside, an accessor property looks like a regular one. That's the
idea of accessor properties. We don't undefinedundefinedcallundefinedundefineduser.fullName as a
function, we undefinedundefinedread it normally: the getter runs behind the scenes.undefinedundefined
As of now, undefinedundefinedfullName has only a getter. If we attempt to assign
undefinedundefineduser.fullName=, there will be an error:undefinedundefined
``undefinedundefinedjs run let user = { get fullName() { return…`; } };undefinedundefined
undefinedundefined! user.fullName = "Test"; // Error (property has only a getter) undefinedundefined/! undefinedundefined
undefinedundefinedLet's fix it by adding a setter for
undefinedundefineduser.fullName:undefinedundefined
run let user = { name: "John", surname: "Smith",
undefinedundefinedget fullName() { return
undefinedundefined${this.name} ${this.surname}; },undefinedundefined
undefinedundefined! set fullName(value) { [this.name, this.surname] = value.split(" "); } undefinedundefined/! };undefinedundefined
undefinedundefined// set fullName is executed with the given value. user.fullName = "Alice Cooper";
undefinedundefinedalert(user.name); // Alice alert(user.surname); // Cooper
undefinedundefinedAs the result, we have a "virtual" property undefinedundefinedfullName. It is
readable and writable.undefinedundefined
Descriptors for accessor properties are different from those for data properties.
undefinedundefinedFor accessor properties, there is no undefinedundefinedvalue or
undefinedundefinedwritable, but instead there are undefinedundefinedget and
undefinedundefinedset functions.undefinedundefined
That is, an accessor descriptor may have:
undefinedundefinedgetundefinedundefined - a function without
arguments, that works when a property is read,undefinedundefinedsetundefinedundefined - a function with one
argument, that is called when the property is set,undefinedundefinedenumerableundefinedundefined - same as for data
properties,undefinedundefinedconfigurableundefinedundefined - same as for data
properties.undefinedundefinedFor instance, to create an accessor
undefinedundefinedfullName with undefinedundefineddefineProperty, we can pass a descriptor
with undefinedundefinedget and undefinedundefinedset:undefinedundefined
run let user = { name: "John", surname: "Smith" };
undefinedundefined
undefinedundefined! Object.defineProperty(user, ‘fullName', { get() { return
undefinedundefined${this.name} ${this.surname}; },undefinedundefined
set(value) { [this.name, this.surname] = value.split(" "); } undefinedundefined/! });undefinedundefined
undefinedundefinedalert(user.fullName); // John Smith
undefinedundefinedfor(let key in user) alert(key); // name, surname
undefinedundefinedPlease note that a property can be either an accessor (has
undefinedundefinedget/set methods) or a data property (has a undefinedundefinedvalue), not
both.undefinedundefined
If we try to supply both undefinedundefinedget and
undefinedundefinedvalue in the same descriptor, there will be an error:undefinedundefined
run undefinedundefined! // Error: Invalid property descriptor. undefinedundefined/! Object.defineProperty({}, ‘prop', { get() { return 1 },undefinedundefined
undefinedundefinedvalue: 2 });
undefinedundefinedGetters/setters can be used as wrappers over "real" property values to gain more control over operations with them.
undefinedundefinedFor instance, if we want to forbid too short names for undefinedundefineduser, we can
have a setter undefinedundefinedname and keep the value in a separate property
undefinedundefined_name:undefinedundefined
run let user = { get name() { return this._name; },
undefinedundefinedset name(value) { if (value.length < 4) { alert("Name is too short, need at least 4 characters"); return; } this._name = value; } };
undefinedundefineduser.name = "Pete"; alert(user.name); // Pete
undefinedundefineduser.name = ""; // Name is too short…
undefinedundefinedSo, the name is stored in undefinedundefined_name property, and the access is done
via getter and setter.undefinedundefined
Technically, external code is able to access the name
directly by using undefinedundefineduser._name. But there is a widely known convention that properties
starting with an underscore undefinedundefined"_" are internal and should not be touched from outside the
object.undefinedundefined
One of the great uses of accessors is that they allow to take control over a "regular" data property at any moment by replacing it with a getter and a setter and tweak its behavior.
undefinedundefined
Imagine we started implementing user objects using data properties undefinedundefinedname and
undefinedundefinedage:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedUser(nameundefinedundefined, age) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedthis.undefinedundefinednameundefinedundefined= nameundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedthis.undefinedundefinedageundefinedundefined= ageundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet john undefinedundefined=undefinedundefinednewundefinedundefinedUser(undefinedundefined"John"undefinedundefined,undefinedundefined25)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefinedjohn.undefinedundefinedage )undefinedundefined;undefinedundefined// 25undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…But sooner or later, things may change. Instead of undefinedundefinedage we may
decide to store undefinedundefinedbirthday, because it's more precise and convenient:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedUser(nameundefinedundefined, birthday) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedthis.undefinedundefinednameundefinedundefined= nameundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedthis.undefinedundefinedbirthdayundefinedundefined= birthdayundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet john undefinedundefined=undefinedundefinednewundefinedundefinedUser(undefinedundefined"John"undefinedundefined,undefinedundefinednewundefinedundefinedDate(undefinedundefined1992undefinedundefined,undefinedundefined6undefinedundefined,undefinedundefined1))undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Now what to do with the old code that still uses undefinedundefinedage
property?undefinedundefined
We can try to find all such places and fix them, but that takes
time and can be hard to do if that code is used by many other people. And besides, undefinedundefinedage
is a nice thing to have in undefinedundefineduser, right?undefinedundefined
Let's keep it.
undefinedundefinedAdding a getter for undefinedundefinedage solves the
problem:undefinedundefined
run no-beautify function User(name, birthday) { this.name = name; this.birthday = birthday;
undefinedundefinedundefinedundefined! // age is calculated from the current date and birthday Object.defineProperty(this, "age", { get() { let todayYear = new Date().getFullYear(); return todayYear - this.birthday.getFullYear(); } }); undefinedundefined/! }undefinedundefined
undefinedundefinedlet john = new User("John", new Date(1992, 6, 1));
undefinedundefinedalert( john.birthday ); // birthday is available alert( john.age ); // …as well as the age
undefinedundefinedNow the old code works too and we've got a nice additional property.
undefinedundefinedIn programming, we often want to take something and extend it.
undefinedundefinedFor instance, we have a undefinedundefineduser object
with its properties and methods, and want to make undefinedundefinedadmin and
undefinedundefinedguest as slightly modified variants of it. We'd like to reuse what we have in
undefinedundefineduser, not copy/reimplement its methods, just build a new object on top of
it.undefinedundefined
undefinedundefinedPrototypal inheritance is a language feature that helps in that.undefinedundefined
undefinedundefined
In JavaScript, objects have a special hidden property undefinedundefined[[Prototype]] (as named in the
specification), that is either undefinedundefinednull or references another object. That object is called
"a prototype":undefinedundefined
When we read a property from undefinedundefinedobject, and it's missing, JavaScript automatically takes
it from the prototype. In programming, such thing is called "prototypal inheritance". And soon we'll study many
examples of such inheritance, as well as cooler language features built upon it.undefinedundefined
The property undefinedundefined[[Prototype]] is internal and hidden, but there are
many ways to set it.undefinedundefined
One of them is to use the special name
undefinedundefined__proto__, like this:undefinedundefined
run let animal = { eats: true }; let rabbit = { jumps: true };
undefinedundefinedundefinedundefined! rabbit.__proto__ = animal; // sets rabbit.[[Prototype]] = animal undefinedundefined/! undefinedundefined
undefinedundefinedNow if we read a property from undefinedundefinedrabbit, and
it's missing, JavaScript will automatically take it from undefinedundefinedanimal.undefinedundefined
For instance:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet animal undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedeatsundefinedundefined:undefinedundefinedtrueundefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedlet rabbit undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedjumpsundefinedundefined:undefinedundefinedtrueundefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedrabbit.undefinedundefined__proto__undefinedundefined= animalundefinedundefined;undefinedundefined// (*)undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// we can find both properties in rabbit now:undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedalertundefinedundefined(undefinedundefined rabbit.eats undefinedundefined)undefinedundefined; // true undefinedundefined(**)undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/undefinedundefined!*undefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefinedrabbit.undefinedundefinedjumps )undefinedundefined;undefinedundefined// trueundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Here the line undefinedundefined(*) sets undefinedundefinedanimal
to be a prototype of undefinedundefinedrabbit.undefinedundefined
Then, when
undefinedundefinedalert tries to read property
undefinedundefinedrabbit.eatsundefinedundefined(**), it's not in
undefinedundefinedrabbit, so JavaScript follows the undefinedundefined[[Prototype]]
reference and finds it in undefinedundefinedanimal (look from the bottom up):undefinedundefined
undefinedundefinedundefinedundefined
Here we can say that "undefinedundefinedanimal is the prototype of undefinedundefinedrabbit"
or "undefinedundefinedrabbit prototypically inherits from
undefinedundefinedanimal".undefinedundefined
So if
undefinedundefinedanimal has a lot of useful properties and methods, then they become automatically
available in undefinedundefinedrabbit. Such properties are called "inherited".undefinedundefined
If we have a method in undefinedundefinedanimal, it can be called on
undefinedundefinedrabbit:undefinedundefined
run let animal = { eats: true, undefinedundefined! walk() { alert("Animal walk"); } undefinedundefined/! };undefinedundefined
undefinedundefinedlet rabbit = { jumps: true, undefinedundefinedproto: animal };undefinedundefined
undefinedundefined// walk is taken from the prototype undefinedundefined! rabbit.walk(); // Animal walk undefinedundefined/! undefinedundefined
undefinedundefinedThe method is automatically taken from the prototype, like this:
undefinedundefinedundefinedundefinedundefinedundefined
The prototype chain can be longer:
undefinedundefinedrun let animal = { eats: true, walk() { alert("Animal walk"); } };
undefinedundefinedlet rabbit = { jumps: true, undefinedundefined!undefinedundefinedproto: animal undefinedundefined/! };undefinedundefined
undefinedundefinedlet longEar = { earLength: 10, undefinedundefined!undefinedundefinedproto: rabbit undefinedundefined/! };undefinedundefined
undefinedundefined// walk is taken from the prototype chain longEar.walk(); // Animal walk alert(longEar.jumps); // true (from rabbit)
undefinedundefinedundefinedundefinedundefinedundefined
Now if we read something from undefinedundefinedlongEar, and it's missing, JavaScript
will look for it in undefinedundefinedrabbit, and then in
undefinedundefinedanimal.undefinedundefined
There are only two limitations:
undefinedundefined__proto__ in a circle.undefinedundefined__proto__ can be either an object or
undefinedundefinednull. Other types are ignored.undefinedundefinedAlso it may be obvious, but still: there can be only one
undefinedundefined[[Prototype]]. An object may not inherit from two others.undefinedundefined
``undefinedundefinedsmart header="undefinedundefinedprotoundefinedundefinedis a historical getter/setter for[[Prototype]]`"
It's a common mistake of novice developers not to know the difference between these two.undefinedundefined
Please note that undefinedundefined__proto__ is undefinedundefinednot the
same as the internal undefinedundefined[[Prototype]] property. It's a getter/setter for
undefinedundefined[[Prototype]]. Later we'll see situations where it matters, for now let's just keep it
in mind, as we build our understanding of JavaScript language.undefinedundefined
The
undefinedundefined__proto__ property is a bit outdated. It exists for historical reasons, modern
JavaScript suggests that we should use undefinedundefinedObject.getPrototypeOf/Object.setPrototypeOf
functions instead that get/set the prototype. We'll also cover these functions later.undefinedundefined
By the specification, undefinedundefined__proto__ must only be supported by browsers.
In fact though, all environments including server-side support undefinedundefined__proto__, so we're
quite safe using it.undefinedundefined
As the undefinedundefined__proto__
notation is a bit more intuitively obvious, we use it in the examples.
undefinedundefined
The prototype is only used for reading properties.
undefinedundefinedWrite/delete operations work directly with the object.
undefinedundefinedIn the example below, we assign its own
undefinedundefinedwalk method to undefinedundefinedrabbit:undefinedundefined
run let animal = { eats: true, walk() { /* this method won't be used by rabbit
*/undefinedundefined
} };undefinedundefined
let rabbit = { undefinedundefinedproto: animal };undefinedundefined
undefinedundefinedundefinedundefined! rabbit.walk = function() { alert("Rabbit! Bounce-bounce!"); }; undefinedundefined/!undefinedundefined
undefinedundefinedrabbit.walk(); // Rabbit! Bounce-bounce!
undefinedundefinedFrom now on, undefinedundefinedrabbit.walk() call finds the method immediately in
the object and executes it, without using the prototype:undefinedundefined
undefinedundefinedundefinedundefined
Accessor properties are an exception, as assignment is handled by a setter function. So writing to such a property is actually the same as calling a function.
undefinedundefinedFor that reason undefinedundefinedadmin.fullName
works correctly in the code below:undefinedundefined
run let user = { name: "John", surname: "Smith",
undefinedundefinedset fullName(value) { [this.name, this.surname] = value.split(" "); },
undefinedundefinedget fullName() { return undefinedundefined${this.name} ${this.surname}; }
};undefinedundefined
let admin = { undefinedundefinedproto: user, isAdmin: true };undefinedundefined
undefinedundefinedalert(admin.fullName); // John Smith (*)
undefinedundefined// setter triggers! admin.fullName = "Alice Cooper"; // (**)
undefinedundefinedalert(admin.fullName); // Alice Cooper, state of admin modified alert(user.fullName); // John Smith, state of user protected
undefinedundefinedHere in the line undefinedundefined(*) the property
undefinedundefinedadmin.fullName has a getter in the prototype undefinedundefineduser, so it
is called. And in the line undefinedundefined(**) the property has a setter in the prototype, so it is
called.undefinedundefined
An interesting question may arise in the example above: what's the value of undefinedundefinedthis
inside undefinedundefinedset fullName(value)? Where are the properties
undefinedundefinedthis.name and undefinedundefinedthis.surname written: into
undefinedundefineduser or undefinedundefinedadmin?undefinedundefined
The answer is simple: undefinedundefinedthis is not affected by prototypes at all.undefinedundefined
undefinedundefinedNo matter where the method is found: in an object or its prototype. In a
method call, undefinedundefinedthis is always the object before the
dot.undefinedundefinedundefinedundefined
So, the setter call
undefinedundefinedadmin.fullName= uses undefinedundefinedadmin as
undefinedundefinedthis, not undefinedundefineduser.undefinedundefined
That is actually a super-important thing, because we may have a big object with many methods, and have objects that inherit from it. And when the inheriting objects run the inherited methods, they will modify only their own states, not the state of the big object.
undefinedundefinedFor instance, here undefinedundefinedanimal
represents a "method storage", and undefinedundefinedrabbit makes use of it.undefinedundefined
The call undefinedundefinedrabbit.sleep() sets
undefinedundefinedthis.isSleeping on the undefinedundefinedrabbit object:undefinedundefined
``undefinedundefinedjs run // animal has methods let animal = { walk() { if (!this.isSleeping) { alert(I
walk`); } }, sleep() { this.isSleeping = true; } };undefinedundefined
let rabbit = { name: "White Rabbit", undefinedundefinedproto: animal };undefinedundefined
undefinedundefined// modifies rabbit.isSleeping rabbit.sleep();
undefinedundefinedalert(rabbit.isSleeping); // true alert(animal.isSleeping); // undefined (no such property in the prototype)
undefinedundefinedThe resulting picture:
undefinedundefinedundefinedundefinedundefinedundefined
If we had other objects, like
undefinedundefinedbird, undefinedundefinedsnake, etc., inheriting from
undefinedundefinedanimal, they would also gain access to methods of
undefinedundefinedanimal. But undefinedundefinedthis in each method call would be the
corresponding object, evaluated at the call-time (before dot), not undefinedundefinedanimal. So when we
write data into undefinedundefinedthis, it is stored into these objects.undefinedundefined
As a result, methods are shared, but the object state is not.
undefinedundefinedThe undefinedundefinedfor..in loop iterates over
inherited properties too.undefinedundefined
For instance:
undefinedundefinedrun let animal = { eats: true };
undefinedundefinedlet rabbit = { jumps: true, undefinedundefinedproto: animal };undefinedundefined
undefinedundefinedundefinedundefined! // Object.keys only returns own keys alert(Object.keys(rabbit)); // jumps undefinedundefined/!undefinedundefined
undefinedundefinedundefinedundefined! // for..in loops over both own and inherited keys for(let prop in rabbit) alert(prop); // jumps, then eats undefinedundefined/! undefinedundefined
undefinedundefinedIf that's not what we want, and we'd like to exclude inherited properties,
there's a built-in method undefinedundefinedobj.hasOwnProperty(key): it
returns undefinedundefinedtrue if undefinedundefinedobj has its own (not inherited) property
named undefinedundefinedkey.undefinedundefined
So we can filter out inherited properties (or do something else with them):
undefinedundefinedrun let animal = { eats: true };
undefinedundefinedlet rabbit = { jumps: true, undefinedundefinedproto: animal };undefinedundefined
undefinedundefinedfor(let prop in rabbit) { let isOwn = rabbit.hasOwnProperty(prop);
undefinedundefinedif
(isOwn) { alert(undefinedundefinedOur: ${prop}); // Our: jumps } else {
alert(undefinedundefinedInherited: ${prop}); // Inherited: eats } }
undefinedundefined
Here we have the following inheritance chain:
undefinedundefinedrabbit inherits from undefinedundefinedanimal, that inherits from
undefinedundefinedObject.prototype (because undefinedundefinedanimal is a literal object
undefinedundefined{...}, so it's by default), and then undefinedundefinednull above
it:undefinedundefined
undefinedundefinedundefinedundefined
Note, there's one funny thing. Where is
the method undefinedundefinedrabbit.hasOwnProperty coming from? We did not define it. Looking at the
chain we can see that the method is provided by undefinedundefinedObject.prototype.hasOwnProperty. In
other words, it's inherited.undefinedundefined
…But why does
undefinedundefinedhasOwnProperty not appear in the undefinedundefinedfor..in loop like
undefinedundefinedeats and undefinedundefinedjumps do, if
undefinedundefinedfor..in lists inherited properties?undefinedundefined
The
answer is simple: it's not enumerable. Just like all other properties of
undefinedundefinedObject.prototype, it has undefinedundefinedenumerable:false flag. And
undefinedundefinedfor..in only lists enumerable properties. That's why it and the rest of the
undefinedundefinedObject.prototype properties are not listed.undefinedundefined
``undefinedundefinedsmart header="Almost all other key/value-getting methods ignore inherited properties" Almost all other key/value-getting methods, such asObject.keysundefinedundefined,Object.values`
and so on ignore inherited properties.undefinedundefined
They only operate on the object itself. Properties from the prototype are undefinedundefinednot taken into account. undefinedundefined
undefinedundefined[[Prototype]] property that's either another
object or undefinedundefinednull.undefinedundefinedobj.__proto__ to access it (a historical getter/setter, there are other ways, to be
covered soon).undefinedundefined[[Prototype]] is called a "prototype".undefinedundefinedobj or call a method, and it doesn't exist, then
JavaScript tries to find it in the prototype.undefinedundefinedobj.method(), and the
undefinedundefinedmethod is taken from the prototype, undefinedundefinedthis still
references undefinedundefinedobj. So methods always work with the current object even if they are
inherited.undefinedundefinedfor..in loop iterates over
both its own and its inherited properties. All other key/value-getting methods only operate on the object
itself.undefinedundefinedRemember, new objects can be created with a constructor function, like
undefinedundefinednew F().undefinedundefined
If
undefinedundefinedF.prototype is an object, then the undefinedundefinednew operator uses it
to set undefinedundefined[[Prototype]] for the new object.undefinedundefined
undefinedundefinedJavaScript had prototypal inheritance from the beginning. It was one of the core features of the language.
But in the old times, there was no direct access to it. The only thing that worked reliably was a `"prototype"` property of the constructor function, described in this chapter. So there are many scripts that still use it.undefinedundefined
undefinedundefinedPlease note that undefinedundefinedF.prototype here means a regular property named
undefinedundefined"prototype" on undefinedundefinedF. It sounds something similar to the
term "prototype", but here we really mean a regular property with this name.undefinedundefined
Here's the example:
undefinedundefinedrun let animal = { eats: true };
undefinedundefinedfunction Rabbit(name) { this.name = name; }
undefinedundefinedundefinedundefined! Rabbit.prototype = animal; undefinedundefined/!undefinedundefined
undefinedundefinedlet rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal
undefinedundefinedalert( rabbit.eats ); // true
undefinedundefinedSetting undefinedundefinedRabbit.prototype = animal literally states the
following: "When a undefinedundefinednew Rabbit is created, assign its
undefinedundefined[[Prototype]] to undefinedundefinedanimal".undefinedundefined
That's the resulting picture:
undefinedundefinedundefinedundefinedundefinedundefined
On the picture,
undefinedundefined"prototype" is a horizontal arrow, meaning a regular property, and
undefinedundefined[[Prototype]] is vertical, meaning the inheritance of
undefinedundefinedrabbit from undefinedundefinedanimal.undefinedundefined
``undefinedundefinedsmart header="F.prototypeundefinedundefinedonly used atnew
Fundefinedundefinedtime"F.prototypeundefinedundefinedproperty is only used whennew
Fundefinedundefinedis called, it assigns[[Prototype]]` of the new object.undefinedundefined
If, after the creation, undefinedundefinedF.prototype property changes
(undefinedundefinedF.prototype = <another object>), then new objects created by
undefinedundefinednew F will have another object as undefinedundefined[[Prototype]], but
already existing objects keep the old one.
undefinedundefined
Every function has the undefinedundefined"prototype"
property even if we don't supply it.undefinedundefined
The default
undefinedundefined"prototype" is an object with the only property
undefinedundefinedconstructor that points back to the function itself.undefinedundefined
Like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedRabbit() undefinedundefined{}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined/* default prototypeundefinedundefinedundefinedundefinedundefinedundefinedRabbit.prototype = { constructor: Rabbit };undefinedundefinedundefinedundefinedundefinedundefined*/undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedundefinedundefined
We can check it:
undefinedundefinedrun function Rabbit() {} // by default: // Rabbit.prototype = { constructor: Rabbit }
undefinedundefinedalert( Rabbit.prototype.constructor == Rabbit ); // true
undefinedundefinedNaturally, if we do nothing, the undefinedundefinedconstructor property is
available to all rabbits through undefinedundefined[[Prototype]]:undefinedundefined
run function Rabbit() {} // by default: // Rabbit.prototype = { constructor: Rabbit }
undefinedundefinedlet rabbit = new Rabbit(); // inherits from {constructor: Rabbit}
undefinedundefinedalert(rabbit.constructor == Rabbit); // true (from prototype)
undefinedundefinedundefinedundefinedundefinedundefined
We can use undefinedundefinedconstructor property to create a new object using the
same constructor as the existing one.undefinedundefined
Like here:
undefinedundefinedrun function Rabbit(name) { this.name = name; alert(name); }
undefinedundefinedlet rabbit = new Rabbit("White Rabbit");
undefinedundefinedundefinedundefined! let rabbit2 = new rabbit.constructor("Black Rabbit"); undefinedundefined/! undefinedundefined
undefinedundefinedThat's handy when we have an object, don't know which constructor was used for it (e.g. it comes from a 3rd party library), and we need to create another one of the same kind.
undefinedundefinedBut probably the most important thing about undefinedundefined"constructor" is
that…undefinedundefined
undefinedundefined…JavaScript itself does not ensure the right
undefinedundefined"constructor" value.undefinedundefinedundefinedundefined
Yes, it exists in the default undefinedundefined"prototype" for functions, but that's
all. What happens with it later - is totally on us.undefinedundefined
In particular, if we
replace the default prototype as a whole, then there will be no undefinedundefined"constructor" in
it.undefinedundefined
For instance:
undefinedundefinedrun function Rabbit() {} Rabbit.prototype = { jumps: true };
undefinedundefinedlet rabbit = new Rabbit(); undefinedundefined! alert(rabbit.constructor === Rabbit); // false undefinedundefined/! undefinedundefined
undefinedundefinedSo, to keep the right undefinedundefined"constructor" we can
choose to add/remove properties to the default undefinedundefined"prototype" instead of overwriting it as
a whole:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedRabbit() undefinedundefined{}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// Not overwrite Rabbit.prototype totallyundefinedundefinedundefinedundefinedundefinedundefined// just add to itundefinedundefinedundefinedundefinedundefinedundefinedRabbit.undefinedundefinedprototype.undefinedundefinedjumpsundefinedundefined=undefinedundefinedtrueundefinedundefinedundefinedundefinedundefinedundefined// the default Rabbit.prototype.constructor is preservedundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Or, alternatively, recreate the undefinedundefinedconstructor property
manually:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedRabbit.undefinedundefinedprototypeundefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedjumpsundefinedundefined:undefinedundefinedtrueundefinedundefined,undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedconstructorundefinedundefined: Rabbitundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// now constructor is also correct, because we added itundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In this chapter we briefly described the way
of setting a undefinedundefined[[Prototype]] for objects created via a constructor function. Later we'll
see more advanced programming patterns that rely on it.undefinedundefined
Everything is quite simple, just a few notes to make things clear:
undefinedundefinedF.prototype property (don't mistake it for
undefinedundefined[[Prototype]]) sets undefinedundefined[[Prototype]] of new objects when
undefinedundefinednew F() is called.undefinedundefinedF.prototype should be either an object or undefinedundefinednull: other
values won't work.undefinedundefined"prototype"
property only has such a special effect when set on a constructor function, and invoked with
undefinedundefinednew.undefinedundefinedOn regular
objects the undefinedundefinedprototype is nothing special:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedprototypeundefinedundefined:undefinedundefined"Bla-bla"undefinedundefined// no magic at allundefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
By default all functions have
undefinedundefinedF.prototype = { constructor: F }, so we can get the constructor of an object by
accessing its undefinedundefined"constructor" property.undefinedundefined
The undefinedundefined"prototype"
property is widely used by the core of JavaScript itself. All built-in constructor functions use it.undefinedundefined
First we'll see at the details, and then how to use it for adding new capabilities to built-in objects.
undefinedundefinedLet's say we output an empty object:
undefinedundefined
undefinedundefinedjs run let obj = {}; alert( obj ); // "[object Object]" ?undefinedundefined
Where's the code that generates the string undefinedundefined"[object Object]"? That's
a built-in undefinedundefinedtoString method, but where is it? The undefinedundefinedobj is
empty!undefinedundefined
…But the short notation undefinedundefinedobj = {} is
the same as undefinedundefinedobj = new Object(), where undefinedundefinedObject is a
built-in object constructor function, with its own undefinedundefinedprototype referencing a huge object
with undefinedundefinedtoString and other methods.undefinedundefined
Here's what's going on:
undefinedundefinedundefinedundefinedundefinedundefined
When undefinedundefinednew Object() is called (or a literal object
undefinedundefined{...} is created), the undefinedundefined[[Prototype]] of it is set to
undefinedundefinedObject.prototype according to the rule that we discussed in the previous
chapter:undefinedundefined
undefinedundefinedundefinedundefined
So then when
undefinedundefinedobj.toString() is called the method is taken from
undefinedundefinedObject.prototype.undefinedundefined
We can check it like this:
undefinedundefinedrun let obj = {};
undefinedundefinedalert(obj.__proto__ === Object.prototype); // true
undefinedundefinedalert(obj.toString === obj.__proto__.toString); //true alert(obj.toString === Object.prototype.toString); //true
undefinedundefinedPlease note that there is no more undefinedundefined[[Prototype]] in the chain
above undefinedundefinedObject.prototype:undefinedundefined
undefinedundefinedjs run alert(Object.prototype.__proto__); // nullundefinedundefined
Other built-in
objects such as undefinedundefinedArray, undefinedundefinedDate,
undefinedundefinedFunction and others also keep methods in prototypes.undefinedundefined
For instance, when we create an array undefinedundefined[1, 2, 3], the default
undefinedundefinednew Array() constructor is used internally. So
undefinedundefinedArray.prototype becomes its prototype and provides methods. That's very
memory-efficient.undefinedundefined
By specification, all of the built-in prototypes have
undefinedundefinedObject.prototype on the top. That's why some people say that "everything inherits from
objects".undefinedundefined
Here's the overall picture (for 3 built-ins to fit):
undefinedundefinedundefinedundefinedundefinedundefined
Let's check the prototypes manually:
undefinedundefinedrun let arr = [1, 2, 3];
undefinedundefined// it inherits from Array.prototype? alert( arr.__proto__ === Array.prototype ); // true
undefinedundefined// then from Object.prototype? alert( arr.__proto__.__proto__ === Object.prototype ); // true
undefinedundefined// and null on the top. alert( arr.__proto__.__proto__.__proto__ ); // null
undefinedundefinedSome methods in prototypes may overlap, for instance,
undefinedundefinedArray.prototype has its own undefinedundefinedtoString that lists
comma-delimited elements:undefinedundefined
undefinedundefinedjs run let arr = [1, 2, 3] alert(arr); // 1,2,3 <-- the result of Array.prototype.toStringundefinedundefined
As we've seen before, undefinedundefinedObject.prototype has
undefinedundefinedtoString as well, but undefinedundefinedArray.prototype is closer in the
chain, so the array variant is used.undefinedundefined
undefinedundefinedundefinedundefined
In-browser tools like Chrome
developer console also show inheritance (undefinedundefinedconsole.dir may need to be used for built-in
objects):undefinedundefined
undefinedundefined
undefinedundefined
Other built-in objects also work the same
way. Even functions - they are objects of a built-in undefinedundefinedFunction constructor, and their
methods (undefinedundefinedcall/undefinedundefinedapply and others) are taken from
undefinedundefinedFunction.prototype. Functions have their own undefinedundefinedtoString
too.undefinedundefined
run function f() {}
undefinedundefinedalert(f.__proto__ == Function.prototype); // true alert(f.__proto__.__proto__ == Object.prototype); // true, inherit from objects
undefinedundefinedThe most intricate thing happens with strings, numbers and booleans.
undefinedundefinedAs we remember, they are not objects. But if we try to access
their properties, temporary wrapper objects are created using built-in constructors
undefinedundefinedString, undefinedundefinedNumber and
undefinedundefinedBoolean. They provide the methods and disappear.undefinedundefined
These objects are created invisibly to us and most engines optimize them out, but the specification
describes it exactly this way. Methods of these objects also reside in prototypes, available as
undefinedundefinedString.prototype, undefinedundefinedNumber.prototype and
undefinedundefinedBoolean.prototype.undefinedundefined
undefinedundefinedwarn header="Values `null` and `undefined` have no object wrappers" Special values `null` and `undefined` stand apart. They have no object wrappers, so methods and properties are not available for them. And there are no corresponding prototypes either.undefinedundefined
Native prototypes can be modified. For instance, if we add a
method to undefinedundefinedString.prototype, it becomes available to all strings:undefinedundefined
run String.prototype.show = function() { alert(this); };
undefinedundefined"BOOM!".show(); // BOOM!
undefinedundefinedDuring the process of development, we may have ideas for new built-in methods we'd like to have, and we may be tempted to add them to native prototypes. But that is generally a bad idea.
undefinedundefinedundefinedundefinedPrototypes are global, so it's easy to get a conflict. If two libraries add a method `String.prototype.show`, then one of them will be overwriting the method of the other.
So, generally, modifying a native prototype is considered a bad idea.undefinedundefinedundefinedundefined
undefinedundefinedIn modern programming, there is only one case where modifying native prototypes is approved. That's polyfilling.undefinedundefined
undefinedundefinedPolyfilling is a term for making a substitute for a method that exists in the JavaScript specification, but is not yet supported by a particular JavaScript engine.
undefinedundefinedWe may then implement it manually and populate the built-in prototype with it.
undefinedundefinedFor instance:
undefinedundefinedrun if (!String.prototype.repeat) { // if there's no such method // add it to the prototype
undefinedundefinedString.prototype.repeat = function(n) { // repeat the string n times
undefinedundefinedundefinedundefined// actually, the code should be a little bit more complex than that
// (the full algorithm is in the specification)
// but even an imperfect polyfill is often considered good enough
return new Array(n + 1).join(this);undefinedundefinedundefinedundefined}; }
undefinedundefinedalert( "La".repeat(3) ); // LaLaLa
undefinedundefinedIn the chapter undefinedundefinedinfo:call-apply-decorators#method-borrowing we talked about method borrowing.undefinedundefined
undefinedundefinedThat's when we take a method from one object and copy it into another.
undefinedundefinedSome methods of native prototypes are often borrowed.
undefinedundefinedFor instance, if we're making an array-like
object, we may want to copy some undefinedundefinedArray methods to it.undefinedundefined
E.g.
undefinedundefinedrun let obj = { 0: "Hello", 1: "world!", length: 2, };
undefinedundefinedundefinedundefined! obj.join = Array.prototype.join; undefinedundefined/!undefinedundefined
undefinedundefinedalert( obj.join(‘,''') ); // Hello,world!
undefinedundefinedIt works because the internal algorithm of the built-in undefinedundefinedjoin
method only cares about the correct indexes and the undefinedundefinedlength property. It doesn't check
if the object is indeed an array. Many built-in methods are like that.undefinedundefined
Another possibility is to inherit by setting undefinedundefinedobj.__proto__ to
undefinedundefinedArray.prototype, so all undefinedundefinedArray methods are automatically
available in undefinedundefinedobj.undefinedundefined
But that's impossible if
undefinedundefinedobj already inherits from another object. Remember, we only can inherit from one object
at a time.undefinedundefined
Borrowing methods is flexible, it allows to mix functionalities from different objects if needed.
undefinedundefinedArray.prototype, undefinedundefinedObject.prototype,
undefinedundefinedDate.prototype, etc.)undefinedundefinedNumber.prototype, undefinedundefinedString.prototype and
undefinedundefinedBoolean.prototype. Only undefinedundefinedundefined and
undefinedundefinednull do not have wrapper objectsundefinedundefinedIn the first chapter of this section, we mentioned that there are modern methods to setup a prototype.
undefinedundefinedThe
undefinedundefined__proto__ is considered outdated and somewhat deprecated (in browser-only part of the
JavaScript standard).undefinedundefined
The modern methods are:
undefinedundefinedproto as
undefinedundefined[[Prototype]] and optional property descriptors.undefinedundefined[[Prototype]] of undefinedundefinedobj.undefinedundefined
[[Prototype]] of undefinedundefinedobj to
undefinedundefinedproto.undefinedundefinedThese should
be used instead of undefinedundefined__proto__.undefinedundefined
For instance:
undefinedundefinedrun let animal = { eats: true };
undefinedundefined// create a new object with animal as a prototype undefinedundefined! let rabbit = Object.create(animal); undefinedundefined/!undefinedundefined
undefinedundefinedalert(rabbit.eats); // true
undefinedundefinedundefinedundefined! alert(Object.getPrototypeOf(rabbit) === animal); // true undefinedundefined/!undefinedundefined
undefinedundefinedundefinedundefined! Object.setPrototypeOf(rabbit, {}); // change the prototype of rabbit to {} undefinedundefined/! undefinedundefined
undefinedundefinedundefinedundefinedObject.create has an optional second
argument: property descriptors. We can provide additional properties to the new object there, like
this:undefinedundefined
run let animal = { eats: true };
undefinedundefinedlet rabbit = Object.create(animal, { jumps: { value: true } });
undefinedundefinedalert(rabbit.jumps); // true
undefinedundefinedThe descriptors are in the same format as described in the chapter undefinedundefinedinfo:property-descriptors.undefinedundefined
undefinedundefinedWe can use undefinedundefinedObject.create to perform an object cloning more powerful than copying
properties in undefinedundefinedfor..in:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet clone undefinedundefined=undefinedundefinedObject.undefinedundefinedcreate(undefinedundefinedObject.undefinedundefinedgetPrototypeOf(obj)undefinedundefined,undefinedundefinedObject.undefinedundefinedgetOwnPropertyDescriptors(obj))undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
This call makes a truly exact copy of undefinedundefinedobj, including all
properties: enumerable and non-enumerable, data properties and setters/getters - everything, and with the right
undefinedundefined[[Prototype]].undefinedundefined
If we count all the ways to manage undefinedundefined[[Prototype]],
there are a lot! Many ways to do the same thing!undefinedundefined
Why?
undefinedundefinedThat's for historical reasons.
undefinedundefined"prototype" property of a constructor function has worked since very ancient
times.undefinedundefinedObject.create appeared in the standard. It gave the ability to create objects with a
given prototype, but did not provide the ability to get/set it. So browsers implemented the non-standard
undefinedundefined__proto__ accessor that allowed the user to get/set a prototype at any
time.undefinedundefinedObject.setPrototypeOf and undefinedundefinedObject.getPrototypeOf were
added to the standard, to perform the same functionality as undefinedundefined__proto__. As
undefinedundefined__proto__ was de-facto implemented everywhere, it was kind-of deprecated and made its
way to the Annex B of the standard, that is: optional for non-browser environments.undefinedundefinedAs of now we have all these ways at our disposal.
undefinedundefined
Why was undefinedundefined__proto__ replaced by the functions
undefinedundefinedgetPrototypeOf/setPrototypeOf? That's an interesting question, requiring us to
understand why undefinedundefined__proto__ is bad. Read on to get the answer.undefinedundefined
``undefinedundefinedwarn header="Don't change[[Prototype]]undefinedundefinedon existing objects if speed matters" Technically, we can get/set[[Prototype]]undefinedundefinedat any time. But usually we only set it once at the object creation time and don't modify it anymore:rabbitundefinedundefinedinherits fromanimal`,
and that is not going to change.undefinedundefined
And JavaScript engines are highly optimized
for this. Changing a prototype "on-the-fly" with undefinedundefinedObject.setPrototypeOf or
undefinedundefinedobj.__proto__= is a very slow operation as it breaks internal optimizations for object
property access operations. So avoid it unless you know what you're doing, or JavaScript speed totally doesn't matter
for you.
undefinedundefined
As we know, objects can be used as associative arrays to store key/value pairs.
undefinedundefined…But if we try to store undefinedundefineduser-provided keys in it (for instance, a
user-entered dictionary), we can see an interesting glitch: all keys work fine except
undefinedundefined"__proto__".undefinedundefined
Check out the example:
undefinedundefinedrun let obj = {};
undefinedundefinedlet key = prompt("What's the key?", "undefinedundefinedproto"); obj[key] = "some value";undefinedundefined
undefinedundefinedalert(obj[key]); // [object Object], not "some value"!
undefinedundefinedHere, if the user types in undefinedundefined__proto__, the assignment is
ignored!undefinedundefined
That shouldn't surprise us. The
undefinedundefined__proto__ property is special: it must be either an object or
undefinedundefinednull. A string can not become a prototype.undefinedundefined
But we didn't undefinedundefinedintend to implement such behavior, right? We want to store key/value pairs,
and the key named undefinedundefined"__proto__" was not properly saved. So that's a
bug!undefinedundefined
Here the consequences are not terrible. But in other cases we may be assigning object values, and then the prototype may indeed be changed. As a result, the execution will go wrong in totally unexpected ways.
undefinedundefinedWhat's worse - usually developers do not think about such possibility at all. That makes such bugs hard to notice and even turn them into vulnerabilities, especially when JavaScript is used on server-side.
undefinedundefinedUnexpected things also may happen when assigning to
undefinedundefinedtoString, which is a function by default, and to other built-in
methods.undefinedundefined
How can we avoid this problem?
undefinedundefinedFirst, we
can just switch to using undefinedundefinedMap for storage instead of plain objects, then everything's
fine.undefinedundefined
But undefinedundefinedObject can also serve us well here,
because language creators gave thought to that problem long ago.undefinedundefined
undefinedundefined__proto__ is not a property of an object, but an accessor property of
undefinedundefinedObject.prototype:undefinedundefined
undefinedundefinedundefinedundefined
So, if
undefinedundefinedobj.__proto__ is read or set, the corresponding getter/setter is called from its
prototype, and it gets/sets undefinedundefined[[Prototype]].undefinedundefined
As
it was said in the beginning of this tutorial section: undefinedundefined__proto__ is a way to access
undefinedundefined[[Prototype]], it is not undefinedundefined[[Prototype]]
itself.undefinedundefined
Now, if we intend to use an object as an associative array and be free of such problems, we can do it with a little trick:
undefinedundefinedrun undefinedundefined! let obj = Object.create(null); undefinedundefined/!undefinedundefined
undefinedundefinedlet key = prompt("What's the key?", "undefinedundefinedproto"); obj[key] = "some value";undefinedundefined
undefinedundefinedalert(obj[key]); // "some value"
undefinedundefinedundefinedundefinedObject.create(null) creates an empty object without a prototype
(undefinedundefined[[Prototype]] is undefinedundefinednull):undefinedundefined
undefinedundefinedundefinedundefined
So, there is no inherited getter/setter for undefinedundefined__proto__. Now it is processed as a
regular data property, so the example above works right.undefinedundefined
We can call such
objects "very plain" or "pure dictionary" objects, because they are even simpler than the regular plain object
undefinedundefined{...}.undefinedundefined
A downside is that such objects lack
any built-in object methods, e.g. undefinedundefinedtoString:undefinedundefined
run undefinedundefined! let obj = Object.create(null); undefinedundefined/!undefinedundefined
undefinedundefinedalert(obj); // Error (no toString)
undefinedundefined…But that's usually fine for associative arrays.
undefinedundefinedNote that most
object-related methods are undefinedundefinedObject.something(...), like
undefinedundefinedObject.keys(obj) - they are not in the prototype, so they will keep working on such
objects:undefinedundefined
run let chineseDictionary = Object.create(null); chineseDictionary.hello = "你好"; chineseDictionary.bye = "再见";
undefinedundefinedalert(Object.keys(chineseDictionary)); // hello,bye
undefinedundefinedModern methods to set up and directly access the prototype are:
undefinedundefinedproto as undefinedundefined[[Prototype]] (can be
undefinedundefinednull) and optional property descriptors.undefinedundefined[[Prototype]] of undefinedundefinedobj (same as
undefinedundefined__proto__ getter).undefinedundefined[[Prototype]] of undefinedundefinedobj to
undefinedundefinedproto (same as undefinedundefined__proto__ setter).undefinedundefined
The built-in undefinedundefined__proto__ getter/setter
is unsafe if we'd want to put user-generated keys into an object. Just because a user may enter
undefinedundefined"__proto__" as the key, and there'll be an error, with hopefully light, but generally
unpredictable consequences.undefinedundefined
So we can either use
undefinedundefinedObject.create(null) to create a "very plain" object without
undefinedundefined__proto__, or stick to undefinedundefinedMap objects for
that.undefinedundefined
Also, undefinedundefinedObject.create provides an easy
way to shallow-copy an object with all descriptors:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet clone undefinedundefined=undefinedundefinedObject.undefinedundefinedcreate(undefinedundefinedObject.undefinedundefinedgetPrototypeOf(obj)undefinedundefined,undefinedundefinedObject.undefinedundefinedgetOwnPropertyDescriptors(obj))undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
We also made it clear that undefinedundefined__proto__ is a getter/setter for
undefinedundefined[[Prototype]] and resides in undefinedundefinedObject.prototype, just like
other methods.undefinedundefined
We can create an object without a prototype by
undefinedundefinedObject.create(null). Such objects are used as "pure dictionaries", they have no issues
with undefinedundefined"__proto__" as the key.undefinedundefined
Other methods:
undefinedundefinedtrue if undefinedundefinedobj has its own (not inherited) key named
undefinedundefinedkey.undefinedundefinedAll methods
that return object properties (like undefinedundefinedObject.keys and others) - return "own" properties.
If we want inherited ones, we can use undefinedundefinedfor..in.undefinedundefined
undefinedundefinedquote author="Wikipedia" In object-oriented programming, a *class* is an extensible program-code-template for creating objects, providing initial values for state (member variables) and implementations of behavior (member functions or methods).undefinedundefined
In practice, we often need to create many objects of the same kind, like users, or goods or whatever.
undefinedundefinedAs we already know from the chapter undefinedundefinedinfo:constructor-new, undefinedundefinednew function can help with
that.undefinedundefined
But in the modern JavaScript, there's a more advanced "class" construct, that introduces great new features which are useful for object-oriented programming.
undefinedundefinedThe basic syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedclass MyClass undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// class methodsundefinedundefinedundefinedundefinedundefinedundefinedconstructor() undefinedundefined{ ... undefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedmethod1() undefinedundefined{ ... undefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedmethod2() undefinedundefined{ ... undefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedmethod3() undefinedundefined{ ... undefinedundefined}undefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Then use undefinedundefinednew MyClass() to create a new object with all the
listed methods.undefinedundefined
The undefinedundefinedconstructor() method is
called automatically by undefinedundefinednew, so we can initialize the object there.undefinedundefined
For example:
undefinedundefinedrun class User {
undefinedundefinedconstructor(name) { this.name = name; }
undefinedundefinedsayHi() { alert(this.name); }
undefinedundefined}
undefinedundefined// Usage: let user = new User("John"); user.sayHi();
undefinedundefinedWhen undefinedundefinednew User("John") is called: 1. A new object is created. 2.
The undefinedundefinedconstructor runs with the given argument and assigns it to
undefinedundefinedthis.name.undefinedundefined
…Then we can call object methods,
such as undefinedundefineduser.sayHi().undefinedundefined
warn header="No comma between class methods" A common pitfall for novice developers is to put a comma between class methods, which would result in a syntax error.
undefinedundefinedThe notation here is not to be confused with object literals. Within the class, no commas are required.
undefinedundefinedSo, what exactly is a
undefinedundefinedclass? That's not an entirely new language-level entity, as one might
think.undefinedundefined
Let's unveil any magic and see what a class really is. That'll help in understanding many complex aspects.
undefinedundefinedIn JavaScript, a class is a kind of function.
undefinedundefinedHere, take a look:
undefinedundefinedrun class User { constructor(name) { this.name = name; } sayHi() { alert(this.name); } }
undefinedundefined// proof: User is a function undefinedundefined! alert(typeof User); // function undefinedundefined/! undefinedundefined
undefinedundefinedWhat undefinedundefinedclass User {...} construct really does
is:undefinedundefined
User, that becomes the result of the class declaration. The function code is taken
from the undefinedundefinedconstructor method (assumed empty if we don't write such
method).undefinedundefinedsayHi, in undefinedundefinedUser.prototype.undefinedundefinedAfter undefinedundefinednew User object is created, when we
call its method, it's taken from the prototype, just as described in the chapter undefinedundefinedinfo:function-prototype. So the object has access to class
methods.undefinedundefined
We can illustrate the result of
undefinedundefinedclass User declaration as:undefinedundefined
undefinedundefinedundefinedundefined
Here's the code to introspect it:
undefinedundefinedrun class User { constructor(name) { this.name = name; } sayHi() { alert(this.name); } }
undefinedundefined// class is a function alert(typeof User); // function
undefinedundefined// …or, more precisely, the constructor method alert(User === User.prototype.constructor); // true
undefinedundefined// The methods are in User.prototype, e.g: alert(User.prototype.sayHi); // the code of the sayHi method
undefinedundefined// there are exactly two methods in the prototype alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi
undefinedundefinedSometimes
people say that undefinedundefinedclass is a "syntactic sugar" (syntax that is designed to make things
easier to read, but doesn't introduce anything new), because we could actually declare the same without
undefinedundefinedclass keyword at all:undefinedundefined
run // rewriting class User in pure functions
undefinedundefined// 1. Create constructor function function User(name) { this.name = name; } // a function prototype has "constructor" property by default, // so we don't need to create it
undefinedundefined// 2. Add the method to prototype User.prototype.sayHi = function() { alert(this.name); };
undefinedundefined// Usage: let user = new User("John"); user.sayHi();
undefinedundefinedThe result of this definition is about the same. So, there are indeed reasons why
undefinedundefinedclass can be considered a syntactic sugar to define a constructor together with its
prototype methods.undefinedundefined
Still, there are important differences.
undefinedundefinedFirst, a function created by
undefinedundefinedclass is labelled by a special internal property
undefinedundefined[[FunctionKind]]:"classConstructor". So it's not entirely the same as creating it
manually.undefinedundefined
The language checks for that property in a variety of places.
For example, unlike a regular function, it must be called with
undefinedundefinednew:undefinedundefined
run class User { constructor() {} }
undefinedundefinedalert(typeof User); // function User(); // Error: Class constructor User cannot be invoked without ‘new'
undefinedundefinedAlso, a string representation of a class constructor in most JavaScript engines starts with the "class…"
undefinedundefinedrun class User { constructor() {} }
undefinedundefinedalert(User); // class User { … } There are other differences, we'll see them soon.
undefinedundefinedClass methods are non-enumerable. A class definition sets
undefinedundefinedenumerable flag to undefinedundefinedfalse for all methods in the
undefinedundefined"prototype".undefinedundefined
That's good, because if we
undefinedundefinedfor..in over an object, we usually don't want its class methods.undefinedundefined
Classes always
undefinedundefineduse strict. All code inside the class construct is automatically in strict
mode.undefinedundefined
Besides,
undefinedundefinedclass syntax brings many other features that we'll explore later.undefinedundefined
Just like functions, classes can be defined inside another expression, passed around, returned, assigned, etc.
undefinedundefinedHere's an example of a class expression:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet User undefinedundefined=undefinedundefinedclassundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedsayHi() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined"Hello")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Similar to Named Function Expressions, class expressions may have a name.
undefinedundefinedIf a class expression has a name, it's visible inside the class only:
undefinedundefinedrun // "Named Class Expression" // (no such term in the spec, but that's similar to Named Function Expression) let User = class undefinedundefined!MyClassundefinedundefined/! { sayHi() { alert(MyClass); // MyClass name is visible only inside the class } };undefinedundefined
undefinedundefinednew User().sayHi(); // works, shows MyClass definition
undefinedundefinedalert(MyClass); // error, MyClass name isn't visible outside of the class
undefinedundefinedWe can even make classes dynamically "on-demand", like this:
undefinedundefinedrun function makeClass(phrase) { // declare a class and return it return class { sayHi() { alert(phrase); } }; }
undefinedundefined// Create a new class let User = makeClass("Hello");
undefinedundefinednew User().sayHi(); // Hello
undefinedundefinedJust like literal objects, classes may include getters/setters, computed properties etc.
undefinedundefinedHere's an example for
undefinedundefineduser.name implemented using undefinedundefinedget/set:undefinedundefined
run class User {
undefinedundefinedconstructor(name) { // invokes the setter this.name = name; }
undefinedundefinedundefinedundefined! get name() { undefinedundefined/! return this._name; }undefinedundefined
undefinedundefinedundefinedundefined! set name(value) { undefinedundefined/! if (value.length < 4) { alert("Name is too short."); return; } this._name = value; }undefinedundefined
undefinedundefined}
undefinedundefinedlet user = new User("John"); alert(user.name); // John
undefinedundefineduser = new User(""); // Name is too short.
undefinedundefinedTechnically, such class declaration works by creating getters and setters in
undefinedundefinedUser.prototype.undefinedundefined
Here's an example with a computed method name using brackets
undefinedundefined[...]:undefinedundefined
run class User {
undefinedundefinedundefinedundefined!undefinedundefined‘say' + ‘Hi' { undefinedundefined/! alert("Hello"); }undefinedundefined
undefinedundefined}
undefinedundefinednew User().sayHi();
undefinedundefinedSuch features are easy to remember, as they resemble that of literal objects.
undefinedundefined
undefinedundefinedwarn header="Old browsers may need a polyfill" Class fields are a recent addition to the language.undefinedundefined
Previously, our classes only had methods.
undefinedundefined"Class fields" is a syntax that allows to add any properties.
undefinedundefinedFor instance, let's add undefinedundefinedname
property to undefinedundefinedclass User:undefinedundefined
run class User { undefinedundefined! name = "John"; undefinedundefined/!undefinedundefined
undefinedundefined
sayHi() { alert(undefinedundefinedHello, ${this.name}!); } }undefinedundefined
new User().sayHi(); // Hello, John!
undefinedundefinedSo, we just write "undefinedundefined
The important difference of class fields is
that they are set on individual objects, not undefinedundefinedUser.prototype:undefinedundefined
run class User { undefinedundefined! name = "John"; undefinedundefined/! }undefinedundefined
undefinedundefinedlet user = new User(); alert(user.name); // John alert(User.prototype.name); // undefined
undefinedundefinedWe can also assign values using more complex expressions and function calls:
undefinedundefinedrun class User { undefinedundefined! name = prompt("Name, please?", "John"); undefinedundefined/! }undefinedundefined
undefinedundefinedlet user = new User(); alert(user.name); // John
undefinedundefinedAs demonstrated in the chapter undefinedundefinedinfo:bind
functions in JavaScript have a dynamic undefinedundefinedthis. It depends on the context of the
call.undefinedundefined
So if an object method is passed around and called in another context,
undefinedundefinedthis won't be a reference to its object any more.undefinedundefined
For instance, this code will show undefinedundefinedundefined:undefinedundefined
run class Button { constructor(value) { this.value = value; }
undefinedundefinedclick() { alert(this.value); } }
undefinedundefinedlet button = new Button("hello");
undefinedundefinedundefinedundefined! setTimeout(button.click, 1000); // undefined undefinedundefined/! undefinedundefined
undefinedundefinedThe problem is called "losing
undefinedundefinedthis".undefinedundefined
There are two approaches to fixing it, as discussed in the chapter undefinedundefinedinfo:bind:undefinedundefined
undefinedundefinedsetTimeout(() => button.click(), 1000).undefinedundefinedClass fields provide another, quite elegant syntax:
undefinedundefinedrun class Button { constructor(value) { this.value = value; } undefinedundefined! click = () => { alert(this.value); } undefinedundefined/! }undefinedundefined
undefinedundefinedlet button = new Button("hello");
undefinedundefinedsetTimeout(button.click, 1000); // hello
undefinedundefinedThe class field undefinedundefinedclick = () => {...} is created on a
per-object basis, there's a separate function for each undefinedundefinedButton object, with
undefinedundefinedthis inside it referencing that object. We can pass
undefinedundefinedbutton.click around anywhere, and the value of undefinedundefinedthis will
always be correct.undefinedundefined
That's especially useful in browser environment, for event listeners.
undefinedundefinedThe basic class syntax looks like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedclass MyClass undefinedundefined{undefinedundefinedundefinedundefined prop undefinedundefined= valueundefinedundefined;undefinedundefined// propertyundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedconstructor(...) undefinedundefined{undefinedundefined// constructorundefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedmethod(...) undefinedundefined{}undefinedundefined// methodundefinedundefinedundefinedundefinedundefinedundefined get undefinedundefinedsomething(...) undefinedundefined{}undefinedundefined// getter methodundefinedundefinedundefinedundefined set undefinedundefinedsomething(...) undefinedundefined{}undefinedundefined// setter methodundefinedundefinedundefinedundefinedundefinedundefined [undefinedundefinedSymbol.undefinedundefinediterator]() undefinedundefined{}undefinedundefined// method with computed name (symbol here)undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedMyClass is technically a function (the one that we provide as
undefinedundefinedconstructor), while methods, getters and setters are written to
undefinedundefinedMyClass.prototype.undefinedundefined
In the next chapters we'll learn more about classes, including inheritance and other features.
undefinedundefinedClass inheritance is a way for one class to extend another class.
undefinedundefinedSo we can create new functionality on top of the existing.
undefinedundefinedLet's say we have class
undefinedundefinedAnimal:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedclass Animal undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedconstructor(name) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedthis.undefinedundefinedspeedundefinedundefined=undefinedundefined0undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedthis.undefinedundefinednameundefinedundefined= nameundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedrun(speed) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedthis.undefinedundefinedspeedundefinedundefined= speedundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`undefinedundefined${undefinedundefinedthis.undefinedundefinednameundefinedundefined}undefinedundefined runs with speed undefinedundefined${undefinedundefinedthis.undefinedundefinedspeedundefinedundefined}undefinedundefined.`)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedstop() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedthis.undefinedundefinedspeedundefinedundefined=undefinedundefined0undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`undefinedundefined${undefinedundefinedthis.undefinedundefinednameundefinedundefined}undefinedundefined stands still.`)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet animal undefinedundefined=undefinedundefinednewundefinedundefinedAnimal(undefinedundefined"My animal")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Here's how we can represent undefinedundefinedanimal object and
undefinedundefinedAnimal class graphically:undefinedundefined
undefinedundefinedundefinedundefined
…And we
would like to create another undefinedundefinedclass Rabbit.undefinedundefined
As
rabbits are animals, undefinedundefinedRabbit class should be based on
undefinedundefinedAnimal, have access to animal methods, so that rabbits can do what "generic" animals
can do.undefinedundefined
The syntax to extend another class is:
undefinedundefinedclass Child extends Parent.undefinedundefined
Let's create
undefinedundefinedclass Rabbit that inherits from
undefinedundefinedAnimal:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedclass Rabbit undefinedundefinedextends Animal undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined hideundefinedundefined()undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined alertundefinedundefined(undefinedundefined`${this.name} hides!`undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet rabbit = new Rabbitundefinedundefined(undefinedundefined"White Rabbit"undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedrabbit.runundefinedundefined(undefinedundefined5undefinedundefined)undefinedundefined; // White Rabbit runs with speed 5.undefinedundefinedundefinedundefinedundefinedundefinedrabbit.hideundefinedundefined()undefinedundefined; // White Rabbit hides!undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Object of undefinedundefinedRabbit class have access both to
undefinedundefinedRabbit methods, such as undefinedundefinedrabbit.hide(), and also to
undefinedundefinedAnimal methods, such as undefinedundefinedrabbit.run().undefinedundefined
Internally, undefinedundefinedextends keyword works using the good old prototype
mechanics. It sets undefinedundefinedRabbit.prototype.[[Prototype]] to
undefinedundefinedAnimal.prototype. So, if a method is not found in
undefinedundefinedRabbit.prototype, JavaScript takes it from
undefinedundefinedAnimal.prototype.undefinedundefined
undefinedundefinedundefinedundefined
For instance, to find
undefinedundefinedrabbit.run method, the engine checks (bottom-up on the picture): 1. The
undefinedundefinedrabbit object (has no undefinedundefinedrun). 2. Its prototype, that is
undefinedundefinedRabbit.prototype (has undefinedundefinedhide, but not
undefinedundefinedrun). 3. Its prototype, that is (due to undefinedundefinedextends)
undefinedundefinedAnimal.prototype, that finally has the undefinedundefinedrun
method.undefinedundefined
As we can recall from the chapter undefinedundefinedinfo:native-prototypes, JavaScript itself uses prototypal inheritance
for built-in objects. E.g. undefinedundefinedDate.prototype.[[Prototype]] is
undefinedundefinedObject.prototype. That's why dates have access to generic object
methods.undefinedundefined
undefinedundefinedsmart header="Any expression is allowed afterextendsundefinedundefined" Class syntax allows to specify not just a class, but any expression afterextends`.undefinedundefined
For instance, a function call that generates the parent class:
undefinedundefinedrun function f(phrase) { return class { sayHi() { alert(phrase); } }; }
undefinedundefinedundefinedundefined! class User extends f("Hello") {} undefinedundefined/!undefinedundefined
undefinedundefinednew User().sayHi(); // Hello
undefinedundefinedundefinedundefinedHere `class User` inherits from the result of `f("Hello")`.
That may be useful for advanced programming patterns when we use functions to generate classes depending on many conditions and can inherit from them.undefinedundefined
undefinedundefinedNow let's move forward and
override a method. By default, all methods that are not specified in undefinedundefinedclass Rabbit are
taken directly "as is" from undefinedundefinedclass Animal.undefinedundefined
But
if we specify our own method in undefinedundefinedRabbit, such as undefinedundefinedstop()
then it will be used instead:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedclass Rabbit undefinedundefinedextends Animal undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedstop() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...now this will be used for rabbit.stop()undefinedundefinedundefinedundefinedundefinedundefined// instead of stop() from class Animalundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Usually we don't want to totally replace a parent method, but rather to build on top of it to tweak or extend its functionality. We do something in our method, but call the parent method before/after it or in the process.
undefinedundefinedClasses provide undefinedundefined"super" keyword for
that.undefinedundefined
super.method(...) to call a parent method.undefinedundefinedsuper(...) to call a parent constructor (inside our constructor
only).undefinedundefinedFor instance, let our rabbit autohide when stopped:
undefinedundefinedrun class Animal {
undefinedundefinedconstructor(name) { this.speed = 0; this.name = name; }
undefinedundefinedrun(speed) { this.speed = speed;
alert(undefinedundefined${this.name} runs with speed ${this.speed}.); }undefinedundefined
stop() { this.speed = 0; alert(undefinedundefined${this.name} stands still.);
}undefinedundefined
}
undefinedundefinedclass Rabbit extends Animal { hide() {
alert(undefinedundefined${this.name} hides!); }undefinedundefined
undefinedundefined! stop() { super.stop(); // call parent stop this.hide(); // and then hide } undefinedundefined/! }undefinedundefined
undefinedundefinedlet rabbit = new Rabbit("White Rabbit");
undefinedundefinedrabbit.run(5); // White Rabbit runs with speed 5. rabbit.stop(); // White Rabbit stands still. White Rabbit hides!
undefinedundefinedNow undefinedundefinedRabbit has the undefinedundefinedstop method
that calls the parent undefinedundefinedsuper.stop() in the process.undefinedundefined
undefinedundefinedsmart header="Arrow functions have nosuperundefinedundefined" As was mentioned in the chapter <info:arrow-functions>, arrow functions do not havesuper`.undefinedundefined
If accessed, it's taken from the outer function. For instance:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedclass Rabbit undefinedundefinedextends Animal undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedstop() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedsetTimeout(() undefinedundefined=>undefinedundefinedsuper.undefinedundefinedstop()undefinedundefined,undefinedundefined1000)undefinedundefined;undefinedundefined// call parent stop after 1secundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The undefinedundefinedsuper in the arrow function is the same as in
undefinedundefinedstop(), so it works as intended. If we specified a "regular" function here, there would
be an error:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// Unexpected superundefinedundefinedundefinedundefinedundefinedundefinedsetTimeout(undefinedundefinedfunction() undefinedundefined{undefinedundefinedsuper.undefinedundefinedstop() undefinedundefined},undefinedundefined1000)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefined
With constructors it gets a little bit tricky.
undefinedundefinedUntil now, undefinedundefinedRabbit did not have its
own undefinedundefinedconstructor.undefinedundefined
According to the
undefinedundefinedspecification, if a class
extends another class and has no undefinedundefinedconstructor, then the following "empty"
undefinedundefinedconstructor is generated:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedclass Rabbit undefinedundefinedextends Animal undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// generated for extending classes without own constructorsundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedconstructor(...undefinedundefinedargs) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedsuper(...undefinedundefinedargs)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
As we can see, it basically calls the parent undefinedundefinedconstructor
passing it all the arguments. That happens if we don't write a constructor of our own.undefinedundefined
Now let's add a custom constructor to undefinedundefinedRabbit. It will specify the
undefinedundefinedearLength in addition to undefinedundefinedname:undefinedundefined
run class Animal { constructor(name) { this.speed = 0; this.name = name; } // … }
undefinedundefinedclass Rabbit extends Animal {
undefinedundefinedundefinedundefined! constructor(name, earLength) { this.speed = 0; this.name = name; this.earLength = earLength; } undefinedundefined/!undefinedundefined
undefinedundefined// … }
undefinedundefinedundefinedundefined! // Doesn't work! let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined. undefinedundefined/! undefinedundefined
undefinedundefinedWhoops! We've got an error. Now we can't create rabbits. What went wrong?
undefinedundefinedThe short answer is:
undefinedundefinedsuper(...),
and (!) do it before using undefinedundefinedthis.undefinedundefinedundefinedundefined…But why? What's going on here? Indeed, the requirement seems strange.
undefinedundefinedOf course, there's an explanation. Let's get into details, so you'll really understand what's going on.
undefinedundefinedIn JavaScript, there's a distinction between a constructor function of an inheriting class
(so-called "derived constructor") and other functions. A derived constructor has a special internal property
undefinedundefined[[ConstructorKind]]:"derived". That's a special internal label.undefinedundefined
That label affects its behavior with undefinedundefinednew.undefinedundefined
new,
it creates an empty object and assigns it to undefinedundefinedthis.undefinedundefinedSo a derived constructor must call
undefinedundefinedsuper in order to execute its parent (base) constructor, otherwise the object for
undefinedundefinedthis won't be created. And we'll get an error.undefinedundefined
For the undefinedundefinedRabbit constructor to work, it needs to call
undefinedundefinedsuper() before using undefinedundefinedthis, like here:undefinedundefined
run class Animal {
undefinedundefinedconstructor(name) { this.speed = 0; this.name = name; }
undefinedundefined// … }
undefinedundefinedclass Rabbit extends Animal {
undefinedundefinedconstructor(name, earLength) { undefinedundefined! super(name); undefinedundefined/! this.earLength = earLength; }undefinedundefined
undefinedundefined// … }
undefinedundefinedundefinedundefined! // now fine let rabbit = new Rabbit("White Rabbit", 10); alert(rabbit.name); // White Rabbit alert(rabbit.earLength); // 10 undefinedundefined/! undefinedundefined
undefinedundefinedwarn header="Advanced note" This note assumes you have a certain experience with classes, maybe in other programming languages.
undefinedundefinedIt provides better insight into the language and also explains the behavior that might be a source of bugs (but not very often).
undefinedundefinedIf you find it difficult to understand, just go on, continue reading, then return to it some time later.
undefinedundefinedWe can override not only methods, but also class fields.
undefinedundefinedAlthough, there's a tricky behavior when we access an overridden field in parent constructor, quite different from most other programming languages.
undefinedundefinedConsider this example:
undefinedundefinedrun class Animal { name = ‘animal';
undefinedundefinedconstructor() { alert(this.name); // (*) } }
undefinedundefinedclass Rabbit extends Animal { name = ‘rabbit'; }
undefinedundefinednew Animal(); // animal undefinedundefined! new Rabbit(); // animal undefinedundefined/! undefinedundefined
undefinedundefinedHere, class undefinedundefinedRabbit extends
undefinedundefinedAnimal and overrides undefinedundefinedname field with its own
value.undefinedundefined
There's no own constructor in undefinedundefinedRabbit,
so undefinedundefinedAnimal constructor is called.undefinedundefined
What's
interesting is that in both cases: undefinedundefinednew Animal() and
undefinedundefinednew Rabbit(), the undefinedundefinedalert in the line
undefinedundefined(*) shows undefinedundefinedanimal.undefinedundefined
undefinedundefinedIn other words, parent constructor always uses its own field value, not the overridden one.undefinedundefined
undefinedundefinedWhat's odd about it?
undefinedundefinedIf it's not clear yet, please compare with methods.
undefinedundefinedHere's the same code, but instead of
undefinedundefinedthis.name field we call undefinedundefinedthis.showName()
method:undefinedundefined
run class Animal { showName() { // instead of this.name = ‘animal' alert(‘animal'); }
undefinedundefinedconstructor() { this.showName(); // instead of alert(this.name); } }
undefinedundefinedclass Rabbit extends Animal { showName() { alert(‘rabbit'); } }
undefinedundefinednew Animal(); // animal undefinedundefined! new Rabbit(); // rabbit undefinedundefined/! undefinedundefined
undefinedundefinedPlease note: now the output is different.
undefinedundefinedAnd that's what we naturally expect. When the parent constructor is called in the derived class, it uses the overridden method.
undefinedundefined…But for class fields it's not so. As said, the parent constructor always uses the parent field.
undefinedundefinedWhy is there the difference?
undefinedundefinedWell, the reason is in the
field initialization order. The class field is initialized: - Before constructor for the base class (that doesn't
extend anything), - Immediately after undefinedundefinedsuper() for the derived class.undefinedundefined
In our case, undefinedundefinedRabbit is the derived class. There's no
undefinedundefinedconstructor() in it. As said previously, that's the same as if there was an empty
constructor with only undefinedundefinedsuper(...args).undefinedundefined
So,
undefinedundefinednew Rabbit() calls undefinedundefinedsuper(), thus executing the parent
constructor, and (per the rule for derived classes) only after that its class fields are initialized. At the time of
the parent constructor execution, there are no undefinedundefinedRabbit class fields yet, that's why
undefinedundefinedAnimal fields are used.undefinedundefined
This subtle difference between fields and methods is specific to JavaScript
undefinedundefinedLuckily, this behavior only reveals itself if an overridden field is used in the parent constructor. Then it may be difficult to understand what's going on, so we're explaining it here.
undefinedundefinedIf it becomes a problem, one can fix it by using methods or getters/setters instead of fields.
undefinedundefinedwarn header="Advanced information" If you're reading the tutorial for the first time - this section may be skipped.
undefinedundefinedIt's about the internal mechanisms behind inheritance and undefinedundefinedsuper.
undefinedundefined
Let's get a little deeper under the hood of
undefinedundefinedsuper. We'll see some interesting things along the way.undefinedundefined
First to say, from all that we've learned till now, it's impossible for
undefinedundefinedsuper to work at all!undefinedundefined
Yeah, indeed, let's ask
ourselves, how it should technically work? When an object method runs, it gets the current object as
undefinedundefinedthis. If we call undefinedundefinedsuper.method() then, the engine needs
to get the undefinedundefinedmethod from the prototype of the current object. But how?undefinedundefined
The task may seem simple, but it isn't. The engine knows the current object
undefinedundefinedthis, so it could get the parent undefinedundefinedmethod as
undefinedundefinedthis.__proto__.method. Unfortunately, such a "naive" solution won't
work.undefinedundefined
Let's demonstrate the problem. Without classes, using plain objects for the sake of simplicity.
undefinedundefinedYou may skip this part and go below to the
undefinedundefined[[HomeObject]] subsection if you don't want to know the details. That won't harm. Or
read on if you're interested in understanding things in-depth.undefinedundefined
In the
example below, undefinedundefinedrabbit.__proto__ = animal. Now let's try: in
undefinedundefinedrabbit.eat() we'll call undefinedundefinedanimal.eat(), using
undefinedundefinedthis.__proto__:undefinedundefined
``undefinedundefinedjs run let animal = { name: "Animal", eat() { alert(${this.name} eats.`); }
};undefinedundefined
let rabbit = { undefinedundefinedproto: animal, name: "Rabbit", eat() { undefinedundefined! // that's how super.eat() could presumably work this.__proto__.eat.call(this); // (undefinedundefined) /!* } };undefinedundefined
undefinedundefinedrabbit.eat(); // Rabbit eats.
undefinedundefinedAt the line undefinedundefined(*) we take undefinedundefinedeat from
the prototype (undefinedundefinedanimal) and call it in the context of the current object. Please note
that undefinedundefined.call(this) is important here, because a simple
undefinedundefinedthis.__proto__.eat() would execute parent undefinedundefinedeat in the
context of the prototype, not the current object.undefinedundefined
And in the code above it
actually works as intended: we have the correct undefinedundefinedalert.undefinedundefined
Now let's add one more object to the chain. We'll see how things break:
undefinedundefined
``undefinedundefinedjs run let animal = { name: "Animal", eat() { alert(${this.name} eats.`); }
};undefinedundefined
let rabbit = { undefinedundefinedproto: animal, eat() { // …bounce around rabbit-style and call parent (animal) method this.__proto__.eat.call(this); // (*) } };undefinedundefined
undefinedundefinedlet longEar = { undefinedundefinedproto: rabbit, eat() { // …do something with long ears and call parent (rabbit) method this.__proto__.eat.call(this); // (**) } };undefinedundefined
undefinedundefinedundefinedundefined! longEar.eat(); // Error: Maximum call stack size exceeded undefinedundefined/! undefinedundefined
undefinedundefinedThe code doesn't work anymore! We can see the error trying to call
undefinedundefinedlongEar.eat().undefinedundefined
It may be not that obvious,
but if we trace undefinedundefinedlongEar.eat() call, then we can see why. In both lines
undefinedundefined(*) and undefinedundefined(**) the value of
undefinedundefinedthis is the current object (undefinedundefinedlongEar). That's essential:
all object methods get the current object as undefinedundefinedthis, not a prototype or
something.undefinedundefined
So, in both lines undefinedundefined(*) and
undefinedundefined(**) the value of undefinedundefinedthis.__proto__ is exactly the same:
undefinedundefinedrabbit. They both call undefinedundefinedrabbit.eat without going up the
chain in the endless loop.undefinedundefined
Here's the picture of what happens:
undefinedundefinedundefinedundefinedundefinedundefined
longEar.eat(), the line
undefinedundefined(**) calls undefinedundefinedrabbit.eat providing it with
undefinedundefinedthis=longEar.
undefinedundefinedjs // inside longEar.eat() we have this = longEar this.__proto__.eat.call(this) // (**) // becomes longEar.__proto__.eat.call(this) // that is rabbit.eat.call(this);undefinedundefined
Then in the line undefinedundefined(*) of
undefinedundefinedrabbit.eat, we'd like to pass the call even higher in the chain, but
undefinedundefinedthis=longEar, so undefinedundefinedthis.__proto__.eat is again
undefinedundefinedrabbit.eat!undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// inside rabbit.eat() we also have this = longEarundefinedundefinedundefinedundefinedundefinedundefinedthis.undefinedundefined__proto__.undefinedundefinedeat.undefinedundefinedcall(undefinedundefinedthis) undefinedundefined// (*)undefinedundefinedundefinedundefinedundefinedundefined// becomesundefinedundefinedundefinedundefinedundefinedundefinedlongEar.undefinedundefined__proto__.undefinedundefinedeat.undefinedundefinedcall(undefinedundefinedthis)undefinedundefinedundefinedundefinedundefinedundefined// or (again)undefinedundefinedundefinedundefinedundefinedundefinedrabbit.undefinedundefinedeat.undefinedundefinedcall(undefinedundefinedthis)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…So
undefinedundefinedrabbit.eat calls itself in the endless loop, because it can't ascend any
further.undefinedundefined
The problem can't
be solved by using undefinedundefinedthis alone.undefinedundefined
[[HomeObject]]undefinedundefinedTo provide
the solution, JavaScript adds one more special internal property for functions:
undefinedundefined[[HomeObject]].undefinedundefined
When a function is specified
as a class or object method, its undefinedundefined[[HomeObject]] property becomes that
object.undefinedundefined
Then undefinedundefinedsuper uses it to resolve the
parent prototype and its methods.undefinedundefined
Let's see how it works, first with plain objects:
undefinedundefined
``undefinedundefinedjs run let animal = { name: "Animal", eat() { // animal.eat.[[HomeObject]] == animal alert(${this.name}
eats.`); } };undefinedundefined
let rabbit = { undefinedundefinedproto: animal, name: "Rabbit", eat() { // rabbit.eat.[[HomeObject]] == rabbit super.eat(); } };undefinedundefined
undefinedundefinedlet longEar = { undefinedundefinedproto: rabbit, name: "Long Ear", eat() { // longEar.eat.[[HomeObject]] == longEar super.eat(); } };undefinedundefined
undefinedundefinedundefinedundefined! // works correctly longEar.eat(); // Long Ear eats. undefinedundefined/! undefinedundefined
undefinedundefinedIt works as intended, due to undefinedundefined[[HomeObject]]
mechanics. A method, such as undefinedundefinedlongEar.eat, knows its
undefinedundefined[[HomeObject]] and takes the parent method from its prototype. Without any use of
undefinedundefinedthis.undefinedundefined
As we've known before, generally functions are "free", not bound to objects in
JavaScript. So they can be copied between objects and called with another
undefinedundefinedthis.undefinedundefined
The very existence of
undefinedundefined[[HomeObject]] violates that principle, because methods remember their objects.
undefinedundefined[[HomeObject]] can't be changed, so this bond is forever.undefinedundefined
The only place in the language where undefinedundefined[[HomeObject]] is used - is
undefinedundefinedsuper. So, if a method does not use undefinedundefinedsuper, then we can
still consider it free and copy between objects. But with undefinedundefinedsuper things may go
wrong.undefinedundefined
Here's the demo of a wrong undefinedundefinedsuper
result after copying:undefinedundefined
``undefinedundefinedjs run let animal = { sayHi() { alert(I'm an animal`); } };undefinedundefined
// rabbit inherits from animal let rabbit = { undefinedundefinedproto: animal, sayHi() { super.sayHi(); } };undefinedundefined
undefinedundefinedlet plant = { sayHi() { alert("I'm a plant"); } };
undefinedundefined// tree inherits from plant let tree = { undefinedundefinedproto: plant, undefinedundefined! sayHi: rabbit.sayHi // (undefinedundefined) /!* };undefinedundefined
undefinedundefinedundefinedundefined! tree.sayHi(); // I'm an animal (?!?) undefinedundefined/! undefinedundefined
undefinedundefinedA call to undefinedundefinedtree.sayHi() shows "I'm an
animal". Definitely wrong.undefinedundefined
The reason is simple: - In the line
undefinedundefined(*), the method undefinedundefinedtree.sayHi was copied from
undefinedundefinedrabbit. Maybe we just wanted to avoid code duplication? - Its
undefinedundefined[[HomeObject]] is undefinedundefinedrabbit, as it was created in
undefinedundefinedrabbit. There's no way to change undefinedundefined[[HomeObject]]. - The
code of undefinedundefinedtree.sayHi() has undefinedundefinedsuper.sayHi() inside. It goes
up from undefinedundefinedrabbit and takes the method from
undefinedundefinedanimal.undefinedundefined
Here's the diagram of what happens:
undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined[[HomeObject]] is defined for methods both in classes and in plain objects. But for
objects, methods must be specified exactly as undefinedundefinedmethod(), not as
undefinedundefined"method: function()".undefinedundefined
The difference may be non-essential for us, but it's important for JavaScript.
undefinedundefinedIn the example below a non-method
syntax is used for comparison. undefinedundefined[[HomeObject]] property is not set and the inheritance
doesn't work:undefinedundefined
run let animal = { eat: function() { // intentionally writing like this instead of eat() {… // … } };
undefinedundefinedlet rabbit = { undefinedundefinedproto: animal, eat: function() { super.eat(); } };undefinedundefined
undefinedundefinedundefinedundefined! rabbit.eat(); // Error calling super (because there's no [[HomeObject]]) undefinedundefined/! undefinedundefined
undefinedundefinedclass Child extends Parent:
undefinedundefinedChild.prototype.__proto__ will
be undefinedundefinedParent.prototype, so methods are inherited.undefinedundefinedsuper() in undefinedundefinedChild constructor before using
undefinedundefinedthis.undefinedundefinedsuper.method() in a
undefinedundefinedChild method to call undefinedundefinedParent
method.undefinedundefined[[HomeObject]] property. That's how undefinedundefinedsuper resolves
parent methods.undefinedundefinedsuper from one object to another.undefinedundefinedAlso: - Arrow functions don't have their own
undefinedundefinedthis or undefinedundefinedsuper, so they transparently fit into the
surrounding context.undefinedundefined
We can also assign a method to the class function itself, not to its
undefinedundefined"prototype". Such methods are called
undefinedundefinedstatic.undefinedundefined
In a class, they are prepended by
undefinedundefinedstatic keyword, like this:undefinedundefined
run class User { undefinedundefined! static staticMethod() { undefinedundefined/! alert(this === User); } }undefinedundefined
undefinedundefinedUser.staticMethod(); // true
undefinedundefinedThat actually does the same as assigning it as a property directly:
undefinedundefinedrun class User { }
undefinedundefinedUser.staticMethod = function() { alert(this === User); };
undefinedundefinedUser.staticMethod(); // true
undefinedundefinedThe value of undefinedundefinedthis in
undefinedundefinedUser.staticMethod() call is the class constructor undefinedundefinedUser
itself (the "object before dot" rule).undefinedundefined
Usually, static methods are used to implement functions that belong to the class, but not to any particular object of it.
undefinedundefinedFor
instance, we have undefinedundefinedArticle objects and need a function to compare them. A natural
solution would be to add undefinedundefinedArticle.compare method, like this:undefinedundefined
run class Article { constructor(title, date) { this.title = title; this.date = date; }
undefinedundefinedundefinedundefined! static compare(articleA, articleB) { return articleA.date - articleB.date; } undefinedundefined/! }undefinedundefined
undefinedundefined// usage let articles = [ new Article("HTML", new Date(2019, 1, 1)), new Article("CSS", new Date(2019, 0, 1)), new Article("JavaScript", new Date(2019, 11, 1))];
undefinedundefinedundefinedundefined! articles.sort(Article.compare); undefinedundefined/!undefinedundefined
undefinedundefinedalert( articles[0].title ); // CSS
undefinedundefinedHere undefinedundefinedArticle.compare stands "above" articles, as a means to
compare them. It's not a method of an article, but rather of the whole class.undefinedundefined
Another example would be a so-called "factory" method. Imagine, we need few ways to create an article:
undefinedundefinedtitle,
undefinedundefineddate etc).undefinedundefinedThe first way can be implemented by the constructor. And for the second one we can make a static method of the class.
undefinedundefinedLike undefinedundefinedArticle.createTodays() here:undefinedundefined
run class Article { constructor(title, date) { this.title = title; this.date = date; }
undefinedundefinedundefinedundefined! static createTodays() { // remember, this = Article return new this("Today's digest", new Date()); } undefinedundefined/! }undefinedundefined
undefinedundefinedlet article = Article.createTodays();
undefinedundefinedalert( article.title ); // Today's digest
undefinedundefinedNow every time we need to create a today's digest, we can call
undefinedundefinedArticle.createTodays(). Once again, that's not a method of an article, but a method of
the whole class.undefinedundefined
Static methods are also used in database-related classes to search/save/remove entries from the database, like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// assuming Article is a special class for managing articlesundefinedundefinedundefinedundefinedundefinedundefined// static method to remove the article:undefinedundefinedundefinedundefinedundefinedundefinedArticle.undefinedundefinedremove(undefinedundefined{undefinedundefinedidundefinedundefined:undefinedundefined12345undefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
[recent browser=Chrome]
undefinedundefinedStatic properties are also possible, they look like regular class properties, but prepended by
undefinedundefinedstatic:undefinedundefined
run class Article { static publisher = "Ilya Kantor"; }
undefinedundefinedalert( Article.publisher ); // Ilya Kantor
undefinedundefinedThat is the same as a direct assignment to
undefinedundefinedArticle:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedArticle.undefinedundefinedpublisherundefinedundefined=undefinedundefined"Ilya Kantor"undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Static properties and methods are inherited.
undefinedundefinedFor instance, undefinedundefinedAnimal.compare and
undefinedundefinedAnimal.planet in the code below are inherited and accessible as
undefinedundefinedRabbit.compare and undefinedundefinedRabbit.planet:undefinedundefined
run class Animal { static planet = "Earth";
undefinedundefinedconstructor(name, speed) { this.speed = speed; this.name = name; }
undefinedundefinedrun(speed = 0) { this.speed += speed;
alert(undefinedundefined${this.name} runs with speed ${this.speed}.); }undefinedundefined
undefinedundefined! static compare(animalA, animalB) { return animalA.speed - animalB.speed; } undefinedundefined/!undefinedundefined
undefinedundefined}
undefinedundefined//
Inherit from Animal class Rabbit extends Animal { hide() { alert(undefinedundefined${this.name} hides!);
} }undefinedundefined
let rabbits = [ new Rabbit("White Rabbit", 10), new Rabbit("Black Rabbit", 5)];
undefinedundefinedundefinedundefined! rabbits.sort(Rabbit.compare); undefinedundefined/!undefinedundefined
undefinedundefinedrabbits[0].run(); // Black Rabbit runs with speed 5.
undefinedundefinedalert(Rabbit.planet); // Earth
undefinedundefinedNow when we call undefinedundefinedRabbit.compare, the inherited
undefinedundefinedAnimal.compare will be called.undefinedundefined
How does it
work? Again, using prototypes. As you might have already guessed, undefinedundefinedextends gives
undefinedundefinedRabbit the undefinedundefined[[Prototype]] reference to
undefinedundefinedAnimal.undefinedundefined
undefinedundefinedundefinedundefined
So,
undefinedundefinedRabbit extends Animal creates two undefinedundefined[[Prototype]]
references:undefinedundefined
Rabbit function prototypally inherits from undefinedundefinedAnimal
function.undefinedundefinedRabbit.prototype prototypally
inherits from undefinedundefinedAnimal.prototype.undefinedundefinedAs a result, inheritance works both for regular and static methods.
undefinedundefinedHere, let's check that by code:
undefinedundefinedrun class Animal {} class Rabbit extends Animal {}
undefinedundefined// for statics alert(Rabbit.__proto__ === Animal); // true
undefinedundefined// for regular methods alert(Rabbit.prototype.__proto__ === Animal.prototype); // true
undefinedundefinedStatic methods are used for the functionality that belongs to the class "as a whole". It doesn't relate to a concrete class instance.
undefinedundefinedFor
example, a method for comparison undefinedundefinedArticle.compare(article1, article2) or a factory
method undefinedundefinedArticle.createTodays().undefinedundefined
They are
labeled by the word undefinedundefinedstatic in class declaration.undefinedundefined
Static properties are used when we'd like to store class-level data, also not bound to an instance.
undefinedundefinedThe syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedclass MyClass undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedstatic property undefinedundefined= ...undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedstaticundefinedundefinedmethod() undefinedundefined{undefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Technically, static declaration is the same as assigning to the class itself:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedMyClass.undefinedundefinedpropertyundefinedundefined= ...undefinedundefinedundefinedundefinedundefinedundefinedMyClass.undefinedundefinedmethodundefinedundefined= ...undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Static properties and methods are inherited.
undefinedundefinedFor
undefinedundefinedclass B extends A the prototype of the class undefinedundefinedB itself
points to undefinedundefinedA: undefinedundefinedB.[[Prototype]] = A. So if a field is not
found in undefinedundefinedB, the search continues in undefinedundefinedA.undefinedundefined
One of the most important principles of object oriented programming - delimiting internal interface from the external one.
undefinedundefinedThat is "a must" practice in developing anything more complex than a "hello world" app.
undefinedundefinedTo understand this, let's break away from development and turn our eyes into the real world.
undefinedundefinedUsually, devices that we're using are quite complex. But delimiting the internal interface from the external one allows to use them without problems.
undefinedundefinedFor instance, a coffee machine. Simple from outside: a button, a display, a few holes…And, surely, the result - great coffee! :)
undefinedundefined
undefinedundefined
undefinedundefined
But inside… (a picture from the repair manual)
undefinedundefinedundefinedundefined
undefinedundefined
A lot of details. But we can use it without knowing anything.
undefinedundefinedCoffee machines are quite reliable, aren't they? We can use one for years, and only if something goes wrong - bring it for repairs.
undefinedundefinedThe secret of reliability and simplicity of a coffee machine - all details are well-tuned and undefinedundefinedhidden inside.undefinedundefined
undefinedundefinedIf we remove the protective cover from the coffee machine, then using it will be much more complex (where to press?), and dangerous (it can electrocute).
undefinedundefinedAs we'll see, in programming objects are like coffee machines.
undefinedundefinedBut in order to hide inner details, we'll use not a protective cover, but rather special syntax of the language and conventions.
undefinedundefinedIn object-oriented programming, properties and methods are split into two groups:
undefinedundefinedIf we continue the analogy with the coffee machine - what's hidden inside: a boiler tube, heating element, and so on - is its internal interface.
undefinedundefinedAn internal interface is used for the object to work, its details use each other. For instance, a boiler tube is attached to the heating element.
undefinedundefinedBut from the outside a coffee machine is closed by the protective cover, so that no one can reach those. Details are hidden and inaccessible. We can use its features via the external interface.
undefinedundefinedSo, all we need to use an object is to know its external interface. We may be completely unaware how it works inside, and that's great.
undefinedundefinedThat was a general introduction.
undefinedundefinedIn JavaScript, there are two types of object fields (properties and methods):
undefinedundefinedIn many other languages there also exist "protected" fields: accessible only from inside the class and those extending it (like private, but plus access from inheriting classes). They are also useful for the internal interface. They are in a sense more widespread than private ones, because we usually want inheriting classes to gain access to them.
undefinedundefinedProtected fields are not implemented in JavaScript on the language level, but in practice they are very convenient, so they are emulated.
undefinedundefinedNow we'll make a coffee machine in JavaScript with all these types of properties. A coffee machine has a lot of details, we won't model them to stay simple (though we could).
undefinedundefinedLet's make a simple coffee machine class first:
undefinedundefinedrun class CoffeeMachine { waterAmount = 0; // the amount of water inside
undefinedundefinedconstructor(power) { this.power = power; alert(
undefinedundefinedCreated a coffee-machine, power: ${power} ); }undefinedundefined
}
undefinedundefined// create the coffee machine let coffeeMachine = new CoffeeMachine(100);
undefinedundefined// add water coffeeMachine.waterAmount = 200;
undefinedundefinedRight now the properties undefinedundefinedwaterAmount and
undefinedundefinedpower are public. We can easily get/set them from the outside to any
value.undefinedundefined
Let's change undefinedundefinedwaterAmount property to
protected to have more control over it. For instance, we don't want anyone to set it below zero.undefinedundefined
undefinedundefinedProtected properties are usually prefixed with an underscore
undefinedundefined_.undefinedundefinedundefinedundefined
That is not enforced on the language level, but there's a well-known convention between programmers that such properties and methods should not be accessed from the outside.
undefinedundefinedSo our property will be called
undefinedundefined_waterAmount:undefinedundefined
run class CoffeeMachine { _waterAmount = 0;
undefinedundefinedset waterAmount(value) { if (value < 0) { value = 0; } this._waterAmount = value; }
undefinedundefinedget waterAmount() { return this._waterAmount; }
undefinedundefinedconstructor(power) { this._power = power; }
undefinedundefined}
undefinedundefined// create the coffee machine let coffeeMachine = new CoffeeMachine(100);
undefinedundefined// add water coffeeMachine.waterAmount = -10; // Error: Negative water
undefinedundefinedNow the access is under control, so setting the water amount below zero becomes impossible.
undefinedundefinedFor
undefinedundefinedpower property, let's make it read-only. It sometimes happens that a property must be
set at creation time only, and then never modified.undefinedundefined
That's exactly the case for a coffee machine: power never changes.
undefinedundefinedTo do so, we only need to make getter, but not the setter:
undefinedundefinedrun class CoffeeMachine { // …
undefinedundefinedconstructor(power) { this._power = power; }
undefinedundefinedget power() { return this._power; }
undefinedundefined}
undefinedundefined// create the coffee machine let coffeeMachine = new CoffeeMachine(100);
undefinedundefined
alert(undefinedundefinedPower is: ${coffeeMachine.power}W); // Power is: 100Wundefinedundefined
coffeeMachine.power = 25; // Error (no setter)
undefinedundefinedsmart header="Getter/setter functions" Here we used getter/setter syntax.
undefinedundefinedBut most of the time
undefinedundefinedget.../set... functions are preferred, like this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedclass CoffeeMachine undefinedundefined{undefinedundefinedundefinedundefined _waterAmount undefinedundefined=undefinedundefined0undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedsetWaterAmount(value)undefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined if undefinedundefined(undefinedundefinedvalue < 0undefinedundefined)undefinedundefined value = 0;undefinedundefinedundefinedundefinedundefinedundefined this._waterAmount = value;undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined!undefinedundefined*undefinedundefinedgetWaterAmountundefinedundefined()*undefinedundefined/undefinedundefined!*undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedthis.undefinedundefined_waterAmountundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinednewundefinedundefinedCoffeeMachine().undefinedundefinedsetWaterAmount(undefinedundefined100)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That looks a bit longer, but functions are more flexible. They can accept multiple arguments (even if we don't need them right now).
undefinedundefinedOn the other hand, get/set syntax is shorter, so ultimately there's no strict rule, it's up to you to decide.
undefinedundefined
``undefinedundefinedsmart header="Protected fields are inherited" If we inheritclass MegaMachine extends
CoffeeMachineundefinedundefined, then nothing prevents us from accessingthis._waterAmountundefinedundefinedorthis._power`
from the methods of the new class.undefinedundefined
So protected fields are naturally inheritable. Unlike private ones that we'll see below.
undefinedundefined[recent browser=none]
undefinedundefinedThere's a finished JavaScript proposal, almost in the standard, that provides language-level support for private properties and methods.
undefinedundefinedPrivates should start with
undefinedundefined#. They are only accessible from inside the class.undefinedundefined
For instance, here's a private undefinedundefined#waterLimit property and the
water-checking private method undefinedundefined#checkWater:undefinedundefined
run class CoffeeMachine { undefinedundefined! #waterLimit = 200; undefinedundefined/!undefinedundefined
undefinedundefinedundefinedundefined! #fixWaterAmount(value) { if (value < 0) return 0; if (value > this.#waterLimit) return this.#waterLimit; } undefinedundefined/!undefinedundefined
undefinedundefinedsetWaterAmount(value) { this.#waterLimit = this.#fixWaterAmount(value); }
undefinedundefined}
undefinedundefinedlet coffeeMachine = new CoffeeMachine();
undefinedundefinedundefinedundefined! // can't access privates from outside of the class coffeeMachine.#fixWaterAmount(123); // Error coffeeMachine.#waterLimit = 1000; // Error undefinedundefined/! undefinedundefined
undefinedundefinedOn the language level, undefinedundefined# is a special sign
that the field is private. We can't access it from outside or from inheriting classes.undefinedundefined
Private fields do not conflict with public ones. We can have both private
undefinedundefined#waterAmount and public undefinedundefinedwaterAmount fields at the same
time.undefinedundefined
For instance, let's make undefinedundefinedwaterAmount an
accessor for undefinedundefined#waterAmount:undefinedundefined
run class CoffeeMachine {
undefinedundefined#waterAmount = 0;
undefinedundefinedget waterAmount() { return this.#waterAmount; }
undefinedundefinedset waterAmount(value) { if (value < 0) value = 0; this.#waterAmount = value; } }
undefinedundefinedlet machine = new CoffeeMachine();
undefinedundefinedmachine.waterAmount = 100; alert(machine.#waterAmount); // Error
undefinedundefinedUnlike protected ones, private fields are enforced by the language itself. That's a good thing.
undefinedundefinedBut if we inherit from undefinedundefinedCoffeeMachine, then we'll have no direct
access to undefinedundefined#waterAmount. We'll need to rely on
undefinedundefinedwaterAmount getter/setter:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedclass MegaCoffeeMachine undefinedundefinedextends CoffeeMachine undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedmethod() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefinedthis.#waterAmount )undefinedundefined;undefinedundefined// Error: can only access from CoffeeMachineundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In many scenarios such limitation is too severe. If we extend a
undefinedundefinedCoffeeMachine, we may have legitimate reasons to access its internals. That's why
protected fields are used more often, even though they are not supported by the language syntax.undefinedundefined
warn header="Private fields are not available as this[name]" Private fields are special.
undefinedundefinedAs we
know, usually we can access fields using undefinedundefinedthis[name]:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedclass User undefinedundefined{undefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefinedsayHi() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet fieldName undefinedundefined=undefinedundefined"name"undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`Hello, undefinedundefined${undefinedundefined*!*undefinedundefinedthis[fieldName]undefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefined}`undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
With private fields that's impossible: undefinedundefinedthis['#name'] doesn't
work. That's a syntax limitation to ensure privacy.
undefinedundefined
In terms of OOP, delimiting of the internal interface from the external one is called undefinedundefinedencapsulation.undefinedundefined
undefinedundefinedIt gives the following benefits:
undefinedundefinedImagine, there's a team of developers using a coffee machine. It was made by the "Best CoffeeMachine" company, and works fine, but a protective cover was removed. So the internal interface is exposed.
undefinedundefinedAll developers are civilized - they use the coffee machine as intended. But one of them, John, decided that he's the smartest one, and made some tweaks in the coffee machine internals. So the coffee machine failed two days later.
undefinedundefinedThat's surely not John's fault, but rather the person who removed the protective cover and let John do his manipulations.
undefinedundefinedThe same in programming. If a user of a class will change things not intended to be changed from the outside - the consequences are unpredictable.
undefinedundefinedThe situation in programming is more complex than with a real-life coffee machine, because we don't just buy it once. The code constantly undergoes development and improvement.
undefinedundefinedundefinedundefinedIf we strictly delimit the internal interface, then the developer of the class can freely change its internal properties and methods, even without informing the users.undefinedundefined
undefinedundefinedIf you're a developer of such class, it's great to know that private methods can be safely renamed, their parameters can be changed, and even removed, because no external code depends on them.
undefinedundefinedFor users, when a new version comes out, it may be a total overhaul internally, but still simple to upgrade if the external interface is the same.
undefinedundefinedPeople adore using things that are simple. At least from outside. What's inside is a different thing.
undefinedundefinedProgrammers are not an exception.
undefinedundefinedundefinedundefinedIt's always convenient when implementation details are hidden, and a simple, well-documented external interface is available.undefinedundefined
undefinedundefinedTo hide an internal interface we use either protected or private properties:
undefinedundefined_. That's a well-known convention, not enforced at the language level. Programmers
should only access a field starting with undefinedundefined_ from its class and classes inheriting from
it.undefinedundefined#.
JavaScript makes sure we can only access those from inside the class.undefinedundefinedRight now, private fields are not well-supported among browsers, but can be polyfilled.
undefinedundefinedFor a long time, JavaScript evolved without compatibility issues. New features were added to the language while old functionality didn't change.
undefinedundefinedThat had the benefit of never breaking existing code. But the downside was that any mistake or an imperfect decision made by JavaScript's creators got stuck in the language forever.
undefinedundefinedThis was the case until 2009 when ECMAScript 5 (ES5) appeared. It added new features to the
language and modified some of the existing ones. To keep the old code working, most such modifications are off by
default. You need to explicitly enable them with a special directive:
undefinedundefined"use strict".undefinedundefined
The directive looks like a string: undefinedundefined"use strict" or
undefinedundefined'use strict'. When it is located at the top of a script, the whole script works the
"modern" way.undefinedundefined
For example:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined"use strict"undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// this code works the modern wayundefinedundefinedundefinedundefined...undefinedundefinedundefinedundefined
undefinedundefined
Quite soon we're going to learn functions (a way to group commands), so let's note in advance
that undefinedundefined"use strict" can be put at the beginning of a function. Doing that enables strict
mode in that function only. But usually people use it for the whole script.undefinedundefined
undefinedundefinedwarn header="Ensure that \"use strict\" is at the top" Please make sure that"use
strict"` is at the top of your scripts, otherwise strict mode may not be enabled.undefinedundefined
Strict mode isn't enabled here:
undefinedundefinedno-strict alert("some code"); // "use strict" below is ignored-it must be at the top
undefinedundefined"use strict";
undefinedundefined// strict mode is not activated
undefinedundefinedundefinedundefined
Only comments may appear above `"use strict"`.undefinedundefinedundefinedundefined
``undefinedundefinedwarn header="There's no way to canceluse
strictundefinedundefined" There is no directive like"no use strict"` that reverts the engine to old
behavior.undefinedundefined
Once we enter strict mode, there's no going back.
undefinedundefinedWhen you use a
undefinedundefineddeveloper console to run code, please note that it doesn't
undefinedundefineduse strict by default.undefinedundefined
Sometimes, when
undefinedundefineduse strict makes a difference, you'll get incorrect results.undefinedundefined
So, how to actually undefinedundefineduse strict in the console?undefinedundefined
First, you can try to press undefinedundefinedkey:Shift+Enter to input multiple lines,
and put undefinedundefineduse strict on top, like this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined'use strict'undefinedundefined;undefinedundefined<Shiftundefinedundefined+Enter undefinedundefinedfor a newlineundefinedundefined>undefinedundefinedundefinedundefinedundefinedundefined// ...your codeundefinedundefinedundefinedundefinedundefinedundefined<Enter to runundefinedundefined>undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
It works in most browsers, namely Firefox and Chrome.
undefinedundefinedIf it doesn't,
e.g. in an old browser, there's an ugly, but reliable way to ensure undefinedundefineduse strict. Put it
inside this kind of wrapper:undefinedundefined
undefinedundefinedundefinedundefined(undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined'use strict'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// ...your code here...undefinedundefinedundefinedundefinedundefinedundefined})()undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The question may sound obvious, but it's not so.
undefinedundefinedOne could recommend to start scripts with
undefinedundefined"use strict"… But you know what's cool?undefinedundefined
Modern JavaScript supports "classes" and "modules" - advanced language structures (we'll surely get to them), that
enable undefinedundefineduse strict automatically. So we don't need to add the
undefinedundefined"use strict" directive, if we use them.undefinedundefined
undefinedundefinedSo, for now undefinedundefined"use strict"; is a welcome guest at the top of
your scripts. Later, when your code is all in classes and modules, you may omit
it.undefinedundefinedundefinedundefined
As of now, we've got to know about
undefinedundefineduse strict in general.undefinedundefined
In the next chapters, as we learn language features, we'll see the differences between the strict and old modes. Luckily, there aren't many and they actually make our lives better.
undefinedundefinedAll examples in this tutorial assume strict mode unless (very rarely) specified otherwise.
undefinedundefinedBuilt-in classes like Array, Map and others are extendable also.
undefinedundefinedFor instance, here undefinedundefinedPowerArray inherits from the native
undefinedundefinedArray:undefinedundefined
run // add one more method to it (can do more) class PowerArray extends Array { isEmpty() { return this.length === 0; } }
undefinedundefinedlet arr = new PowerArray(1, 2, 5, 10, 50); alert(arr.isEmpty()); // false
undefinedundefinedlet filteredArr = arr.filter(item => item >= 10); alert(filteredArr); // 10, 50 alert(filteredArr.isEmpty()); // false
undefinedundefinedPlease note a very interesting thing. Built-in methods like
undefinedundefinedfilter, undefinedundefinedmap and others - return new objects of exactly
the inherited type undefinedundefinedPowerArray. Their internal implementation uses the object's
undefinedundefinedconstructor property for that.undefinedundefined
In the example above,
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedarr.undefinedundefinedconstructorundefinedundefined=== PowerArrayundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
When undefinedundefinedarr.filter() is called, it internally creates the new
array of results using exactly undefinedundefinedarr.constructor, not basic
undefinedundefinedArray. That's actually very cool, because we can keep using
undefinedundefinedPowerArray methods further on the result.undefinedundefined
Even more, we can customize that behavior.
undefinedundefinedWe can add a special static getter
undefinedundefinedSymbol.species to the class. If it exists, it should return the constructor that
JavaScript will use internally to create new entities in undefinedundefinedmap,
undefinedundefinedfilter and so on.undefinedundefined
If we'd like built-in
methods like undefinedundefinedmap or undefinedundefinedfilter to return regular arrays, we
can return undefinedundefinedArray in undefinedundefinedSymbol.species, like
here:undefinedundefined
run class PowerArray extends Array { isEmpty() { return this.length === 0; }
undefinedundefinedundefinedundefined! // built-in methods will use this as the constructor static get undefinedundefinedSymbol.species { return Array; } undefinedundefined/! }undefinedundefined
undefinedundefinedlet arr = new PowerArray(1, 2, 5, 10, 50); alert(arr.isEmpty()); // false
undefinedundefined// filter creates new array using arr.constructor[Symbol.species] as constructor let filteredArr = arr.filter(item => item >= 10);
undefinedundefinedundefinedundefined! // filteredArr is not PowerArray, but Array undefinedundefined/! alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function undefinedundefined
undefinedundefinedAs you can see, now undefinedundefined.filter returns
undefinedundefinedArray. So the extended functionality is not passed any further.undefinedundefined
undefinedundefinedsmart header="Other collections work similarly" Other collections, such as `Map` and `Set`, work alike. They also use `Symbol.species`.undefinedundefined
Built-in objects have their own static methods, for instance
undefinedundefinedObject.keys, undefinedundefinedArray.isArray etc.undefinedundefined
As we already know, native classes extend each other. For instance,
undefinedundefinedArray extends undefinedundefinedObject.undefinedundefined
Normally, when one class extends another, both static and non-static methods are inherited. That was thoroughly explained in the article undefinedundefined.undefinedundefined
undefinedundefinedBut built-in classes are an exception. They don't inherit statics from each other.
undefinedundefinedFor example,
both undefinedundefinedArray and undefinedundefinedDate inherit from
undefinedundefinedObject, so their instances have methods from
undefinedundefinedObject.prototype. But undefinedundefinedArray.[[Prototype]] does not
reference undefinedundefinedObject, so there's no, for instance,
undefinedundefinedArray.keys() (or undefinedundefinedDate.keys()) static
method.undefinedundefined
Here's the picture structure for undefinedundefinedDate
and undefinedundefinedObject:undefinedundefined
undefinedundefinedundefinedundefined
As you can see, there's no link
between undefinedundefinedDate and undefinedundefinedObject. They are independent, only
undefinedundefinedDate.prototype inherits from
undefinedundefinedObject.prototype.undefinedundefined
That's an important
difference of inheritance between built-in objects compared to what we get with
undefinedundefinedextends.undefinedundefined
The undefinedundefinedinstanceof operator allows to
check whether an object belongs to a certain class. It also takes inheritance into account.undefinedundefined
Such a check may be necessary in many cases. For example, it can be used for building a undefinedundefinedpolymorphic function, the one that treats arguments differently depending on their type.undefinedundefined
undefinedundefinedThe syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedobj undefinedundefinedinstanceof Classundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
It returns undefinedundefinedtrue if undefinedundefinedobj belongs
to the undefinedundefinedClass or a class inheriting from it.undefinedundefined
For instance:
undefinedundefinedrun class Rabbit {} let rabbit = new Rabbit();
undefinedundefined// is it an object of Rabbit class? undefinedundefined! alert( rabbit instanceof Rabbit ); // true undefinedundefined/! undefinedundefined
undefinedundefinedIt also works with constructor functions:
undefinedundefinedrun undefinedundefined! // instead of class function Rabbit() {} undefinedundefined/!undefinedundefined
undefinedundefinedalert( new Rabbit() instanceof Rabbit ); // true
undefinedundefined…And with built-in classes like undefinedundefinedArray:undefinedundefined
undefinedundefinedjs run let arr = [1, 2, 3]; alert( arr instanceof Array ); // true alert( arr instanceof Object ); // trueundefinedundefined
Please note that undefinedundefinedarr also belongs to the
undefinedundefinedObject class. That's because undefinedundefinedArray prototypically
inherits from undefinedundefinedObject.undefinedundefined
Normally,
undefinedundefinedinstanceof examines the prototype chain for the check. We can also set a custom logic
in the static method undefinedundefinedSymbol.hasInstance.undefinedundefined
The
algorithm of undefinedundefinedobj instanceof Class works roughly as follows:undefinedundefined
If there's a static method
undefinedundefinedSymbol.hasInstance, then just call it:
undefinedundefinedClass[Symbol.hasInstance](obj). It should return either
undefinedundefinedtrue or undefinedundefinedfalse, and we're done. That's how we can
customize the behavior of undefinedundefinedinstanceof.undefinedundefined
For example:
undefinedundefinedrun // setup instanceOf check that assumes that // anything with canEat property is an animal class Animal { static undefinedundefinedSymbol.hasInstance { if (obj.canEat) return true; } }undefinedundefined
undefinedundefinedlet obj = { canEat: true };
undefinedundefinedalert(obj instanceof Animal); // true: AnimalundefinedundefinedSymbol.hasInstance is called undefinedundefined
undefinedundefinedMost classes do not have
undefinedundefinedSymbol.hasInstance. In that case, the standard logic is used:
undefinedundefinedobj instanceOf Class checks whether undefinedundefinedClass.prototype
is equal to one of the prototypes in the undefinedundefinedobj prototype chain.undefinedundefined
In other words, compare one after another:
undefinedundefinedjs obj.__proto__ === Class.prototype? obj.__proto__.__proto__ === Class.prototype? obj.__proto__.__proto__.__proto__ === Class.prototype? ... // if any answer is true, return true // otherwise, if we reached the end of the chain, return falseundefinedundefined
In the example above undefinedundefinedrabbit.__proto__ === Rabbit.prototype,
so that gives the answer immediately.undefinedundefined
In the case of an inheritance, the match will be at the second step:
undefinedundefinedrun class Animal {} class Rabbit extends Animal {}
undefinedundefinedlet rabbit = new Rabbit(); undefinedundefined! alert(rabbit instanceof Animal); // true undefinedundefined/!undefinedundefined
undefinedundefined// rabbit.__proto__ === Rabbit.prototype undefinedundefined! // rabbit.__proto__.__proto__ === Animal.prototype (match!) undefinedundefined/! undefinedundefined
undefinedundefinedHere's the illustration
of what undefinedundefinedrabbit instanceof Animal compares with
undefinedundefinedAnimal.prototype:undefinedundefined
undefinedundefinedundefinedundefined
By the way, there's also a method
undefinedundefinedobjA.isPrototypeOf(objB), that returns
undefinedundefinedtrue if undefinedundefinedobjA is somewhere in the chain of prototypes for
undefinedundefinedobjB. So the test of undefinedundefinedobj instanceof Class can be
rephrased as undefinedundefinedClass.prototype.isPrototypeOf(obj).undefinedundefined
It's funny, but the undefinedundefinedClass constructor itself does not participate in
the check! Only the chain of prototypes and undefinedundefinedClass.prototype matters.undefinedundefined
That can lead to interesting consequences when a undefinedundefinedprototype
property is changed after the object is created.undefinedundefined
Like here:
undefinedundefinedrun function Rabbit() {} let rabbit = new Rabbit();
undefinedundefined// changed the prototype Rabbit.prototype = {};
undefinedundefined// …not a rabbit any more! undefinedundefined! alert( rabbit instanceof Rabbit ); // false undefinedundefined/! undefinedundefined
undefinedundefinedWe already know that plain objects are converted to
string as undefinedundefined[object Object]:undefinedundefined
run let obj = {};
undefinedundefinedalert(obj); // [object Object] alert(obj.toString()); // the same
undefinedundefinedThat's their implementation of undefinedundefinedtoString. But there's a hidden
feature that makes undefinedundefinedtoString actually much more powerful than that. We can use it as an
extended undefinedundefinedtypeof and an alternative for
undefinedundefinedinstanceof.undefinedundefined
Sounds strange? Indeed. Let's demystify.
undefinedundefinedBy undefinedundefinedspecification, the built-in
undefinedundefinedtoString can be extracted from the object and executed in the context of any other
value. And its result depends on that value.undefinedundefined
[object Number]undefinedundefined[object Boolean]undefinedundefinednull: undefinedundefined[object Null]undefinedundefinedundefined:
undefinedundefined[object Undefined]undefinedundefined[object Array]undefinedundefinedLet's demonstrate:
undefinedundefinedrun // copy toString method into a variable for convenience let objectToString = Object.prototype.toString;
undefinedundefined// what type is this? let arr = [];
undefinedundefinedalert( objectToString.call(arr) ); // [object undefinedundefined!Arrayundefinedundefined/!] undefinedundefined
undefinedundefinedHere we used undefinedundefinedcall as
described in the chapter undefinedundefined to execute the function
undefinedundefinedobjectToString in the context
undefinedundefinedthis=arr.undefinedundefined
Internally, the
undefinedundefinedtoString algorithm examines undefinedundefinedthis and returns the
corresponding result. More examples:undefinedundefined
run let s = Object.prototype.toString;
undefinedundefinedalert( s.call(123) ); // [object Number] alert( s.call(null) ); // [object Null] alert( s.call(alert) ); // [object Function]
undefinedundefinedThe behavior of Object
undefinedundefinedtoString can be customized using a special object property
undefinedundefinedSymbol.toStringTag.undefinedundefined
For instance:
undefinedundefinedrun let user = { undefinedundefinedSymbol.toStringTag: "User" };undefinedundefined
undefinedundefinedalert( {}.toString.call(user) ); // [object User]
undefinedundefinedFor most environment-specific objects, there is such a property. Here are some browser specific examples:
undefinedundefinedrun // toStringTag for the environment-specific object and class: alert( windowundefinedundefinedSymbol.toStringTag); // Window alert( XMLHttpRequest.prototypeundefinedundefinedSymbol.toStringTag ); // XMLHttpRequestundefinedundefined
undefinedundefinedalert( {}.toString.call(window) ); // [object Window] alert( {}.toString.call(new XMLHttpRequest()) ); // [object XMLHttpRequest]
undefinedundefinedAs you can see, the result is exactly undefinedundefinedSymbol.toStringTag (if
exists), wrapped into undefinedundefined[object ...].undefinedundefined
At the end we have "typeof on steroids" that not only works for primitive data types, but also for built-in objects and even can be customized.
undefinedundefinedWe can use undefinedundefined{}.toString.call instead of
undefinedundefinedinstanceof for built-in objects when we want to get the type as a string rather than
just to check.undefinedundefined
Let's summarize the type-checking methods that we know:
undefinedundefined| undefinedundefined | works for | undefinedundefinedreturns | undefinedundefined
|---|---|---|
undefinedundefinedtypeofundefinedundefined | undefinedundefinedprimitives | undefinedundefinedstring | undefinedundefined
undefinedundefined{}.toStringundefinedundefined | undefinedundefinedprimitives, built-in
objects, objects with undefinedundefinedSymbol.toStringTagundefinedundefined | undefinedundefined
string | undefinedundefined
undefinedundefinedinstanceofundefinedundefined | undefinedundefinedobjects | undefinedundefinedtrue/false | undefinedundefined
As we can see, undefinedundefined{}.toString is technically a "more advanced"
undefinedundefinedtypeof.undefinedundefined
And
undefinedundefinedinstanceof operator really shines when we are working with a class hierarchy and want
to check for the class taking into account inheritance.undefinedundefined
In JavaScript we can only inherit from a single object. There can be only one
undefinedundefined[[Prototype]] for an object. And a class may extend only one other
class.undefinedundefined
But sometimes that feels limiting. For instance, we have a class
undefinedundefinedStreetSweeper and a class undefinedundefinedBicycle, and want to make
their mix: a undefinedundefinedStreetSweepingBicycle.undefinedundefined
Or we
have a class undefinedundefinedUser and a class undefinedundefinedEventEmitter that
implements event generation, and we'd like to add the functionality of undefinedundefinedEventEmitter to
undefinedundefinedUser, so that our users can emit events.undefinedundefined
There's a concept that can help here, called "mixins".
undefinedundefinedAs defined in Wikipedia, a undefinedundefinedmixin is a class containing methods that can be used by other classes without a need to inherit from it.undefinedundefined
undefinedundefinedIn other words, a undefinedundefinedmixin provides methods that implement a certain behavior, but we do not use it alone, we use it to add the behavior to other classes.undefinedundefined
undefinedundefinedThe simplest way to implement a mixin in JavaScript is to make an object with useful methods, so that we can easily merge them into a prototype of any class.
undefinedundefinedFor instance here the
mixin undefinedundefinedsayHiMixin is used to add some "speech" for
undefinedundefinedUser:undefinedundefined
``undefinedundefinedjs run *!* // mixin */!* let sayHiMixin = { sayHi() { alert(Hello
${this.name}undefinedundefined); }, sayBye() { alert(Bye ${this.name}`); } };undefinedundefined
undefinedundefined! // usage: undefinedundefined/! class User { constructor(name) { this.name = name; } }undefinedundefined
undefinedundefined// copy the methods Object.assign(User.prototype, sayHiMixin);
undefinedundefined// now User can say hi new User("Dude").sayHi(); // Hello Dude!
undefinedundefinedThere's no inheritance, but a simple method copying. So undefinedundefinedUser may
inherit from another class and also include the mixin to "mix-in" the additional methods, like this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedclass User undefinedundefinedextends Person undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedObject.undefinedundefinedassign(undefinedundefinedUser.undefinedundefinedprototypeundefinedundefined, sayHiMixin)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Mixins can make use of inheritance inside themselves.
undefinedundefinedFor instance,
here undefinedundefinedsayHiMixin inherits from
undefinedundefinedsayMixin:undefinedundefined
run let sayMixin = { say(phrase) { alert(phrase); } };
undefinedundefinedlet sayHiMixin = { undefinedundefinedproto: sayMixin, // (or we could use Object.setPrototypeOf to set the prototype here)undefinedundefined
undefinedundefinedsayHi() {
undefinedundefined! // call parent method undefinedundefined/!
super.say(undefinedundefinedHello ${this.name}); // (undefinedundefined) }, sayBye() {
super.say(undefinedundefinedBye ${this.name}); // (undefinedundefined) } };undefinedundefined
class User { constructor(name) { this.name = name; } }
undefinedundefined// copy the methods Object.assign(User.prototype, sayHiMixin);
undefinedundefined// now User can say hi new User("Dude").sayHi(); // Hello Dude!
undefinedundefinedPlease note that the call to the parent method undefinedundefinedsuper.say() from
undefinedundefinedsayHiMixin (at lines labelled with undefinedundefined(*)) looks for the
method in the prototype of that mixin, not the class.undefinedundefined
Here's the diagram (see the right part):
undefinedundefinedundefinedundefinedundefinedundefined
That's because methods undefinedundefinedsayHi and
undefinedundefinedsayBye were initially created in undefinedundefinedsayHiMixin. So even
though they got copied, their undefinedundefined[[HomeObject]] internal property references
undefinedundefinedsayHiMixin, as shown in the picture above.undefinedundefined
As
undefinedundefinedsuper looks for parent methods in
undefinedundefined[[HomeObject]].[[Prototype]], that means it searches
undefinedundefinedsayHiMixin.[[Prototype]], not
undefinedundefinedUser.[[Prototype]].undefinedundefined
Now let's make a mixin for real life.
undefinedundefinedAn important feature of many browser objects (for instance) is that they can generate events. Events are a great way to "broadcast information" to anyone who wants it. So let's make a mixin that allows us to easily add event-related functions to any class/object.
undefinedundefined.trigger(name, [...data]) to "generate an event" when something important happens to
it. The undefinedundefinedname argument is a name of the event, optionally followed by additional
arguments with event data.undefinedundefined.on(name, handler) that adds undefinedundefinedhandler function as the
listener to events with the given name. It will be called when an event with the given
undefinedundefinedname triggers, and get the arguments from the undefinedundefined.trigger
call.undefinedundefined.off(name, handler)
that removes the undefinedundefinedhandler listener.undefinedundefinedAfter adding the mixin, an object undefinedundefineduser will be able to generate an
event undefinedundefined"login" when the visitor logs in. And another object, say,
undefinedundefinedcalendar may want to listen for such events to load the calendar for the logged-in
person.undefinedundefined
Or, a undefinedundefinedmenu can generate the event
undefinedundefined"select" when a menu item is selected, and other objects may assign handlers to react
on that event. And so on.undefinedundefined
Here's the code:
undefinedundefinedrun let eventMixin = { /** * Subscribe to event, usage: * menu.on(‘select', function(item) { … } */ on(eventName, handler) { if (!this._eventHandlers) this._eventHandlers = {}; if (!this._eventHandlers[eventName]) { this._eventHandlers[eventName] = []; } this._eventHandlers[eventName].push(handler); },
undefinedundefined/** * Cancel the subscription, usage: * menu.off(‘select', handler) */ off(eventName, handler) { let handlers = this._eventHandlers?.[eventName]; if (!handlers) return; for (let i = 0; i < handlers.length; i++) { if (handlers[i] === handler) { handlers.splice(i-, 1); } } },
undefinedundefined/** * Generate an event with the given name and data * this.trigger(‘select', data1, data2); */ trigger(eventName, …args) { if (!this._eventHandlers?.[eventName]) { return; // no handlers for that event name }
undefinedundefinedundefinedundefined// call the handlers
this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));undefinedundefined
undefinedundefined} };
undefinedundefined.on(eventName, handler) - assigns
function undefinedundefinedhandler to run when the event with that name occurs. Technically, there's an
undefinedundefined_eventHandlers property that stores an array of handlers for each event name, and it
just adds it to the list.undefinedundefined.off(eventName, handler) - removes the function from the handlers
list.undefinedundefined.trigger(eventName, ...args) -
generates the event: all handlers from undefinedundefined_eventHandlers[eventName] are called, with a
list of arguments undefinedundefined...args.undefinedundefinedUsage:
undefinedundefinedrun // Make a class class Menu { choose(value) { this.trigger("select", value); } } // Add the mixin with event-related methods Object.assign(Menu.prototype, eventMixin);
undefinedundefinedlet menu = new Menu();
undefinedundefined// add a handler, to be called on
selection: undefinedundefined! menu.on("select", value =>
alert(undefinedundefinedValue selected: ${value})); undefinedundefined/!undefinedundefined
// triggers the event => the handler above runs and shows: // Value selected: 123 menu.choose("123");
undefinedundefinedNow, if we'd like any code to react to a menu selection, we can listen for it with
undefinedundefinedmenu.on(...).undefinedundefined
And
undefinedundefinedeventMixin mixin makes it easy to add such behavior to as many classes as we'd like,
without interfering with the inheritance chain.undefinedundefined
undefinedundefinedMixin - is a generic object-oriented programming term: a class that contains methods for other classes.undefinedundefined
undefinedundefinedSome other languages allow multiple inheritance. JavaScript does not support multiple inheritance, but mixins can be implemented by copying methods into prototype.
undefinedundefinedWe can use mixins as a way to augment a class by adding multiple behaviors, like event-handling as we have seen above.
undefinedundefinedMixins may become a point of conflict if they accidentally overwrite existing class methods. So generally one should think well about the naming methods of a mixin, to minimize the probability of that happening.
undefinedundefinedNo matter how great we are at programming, sometimes our scripts have errors. They may occur because of our mistakes, an unexpected user input, an erroneous server response, and for a thousand other reasons.
undefinedundefinedUsually, a script "dies" (immediately stops) in case of an error, printing it to console.
undefinedundefinedBut there's a syntax construct undefinedundefinedtry...catch that
allows us to "catch" errors so the script can, instead of dying, do something more reasonable.undefinedundefined
The
undefinedundefinedtry...catch construct has two main blocks: undefinedundefinedtry, and then
undefinedundefinedcatch:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedtryundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// code...undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedcatch (err) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// error handlingundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
It works like this:
undefinedundefinedtry {...} is executed.undefinedundefinedcatch (err) is ignored: the execution reaches the end of
undefinedundefinedtry and goes on, skipping undefinedundefinedcatch.undefinedundefined
try execution is stopped, and
control flows to the beginning of undefinedundefinedcatch (err). The undefinedundefinederr
variable (we can use any name for it) will contain an error object with details about what
happened.undefinedundefinedundefinedundefinedundefinedundefined
So, an error inside the
undefinedundefinedtry {...} block does not kill the script - we have a chance to handle it in
undefinedundefinedcatch.undefinedundefined
Let's look at some examples.
undefinedundefinedAn errorless example: shows
undefinedundefinedalertundefinedundefined(1) and
undefinedundefined(2):undefinedundefined
run try {
undefinedundefinedundefinedundefinedalert('Start of try runs'); // *!*(1) <--*/!*
// ...no errors here
alert('End of try runs'); // *!*(2) <--*/!*undefinedundefinedundefinedundefined} catch (err) {
undefinedundefinedundefinedundefinedalert('Catch is ignored, because there are no errors'); // (3)undefinedundefined
}
undefinedundefined
An example with an error: shows undefinedundefined(1) and
undefinedundefined(3):undefinedundefined
run try {
undefinedundefinedundefinedundefinedalert('Start of try runs'); // *!*(1) <--*/!*undefinedundefined
undefinedundefinedundefinedundefined! lalala; // error, variable is not defined! undefinedundefined/!undefinedundefined
undefinedundefinedundefinedundefinedalert('End of try (never reached)'); // (2)undefinedundefined
undefinedundefined} catch (err) {
undefinedundefinedundefinedundefinedalert(`Error has occurred!`); // *!*(3) <--*/!*undefinedundefined
undefinedundefined}
undefinedundefined
undefinedundefinedwarn header="try…catchundefinedundefinedonly works for runtime errors" Fortry…catch`
to work, the code must be runnable. In other words, it should be valid JavaScript.undefinedundefined
It won't work if the code is syntactically wrong, for instance it has unmatched curly braces:
undefinedundefined
undefinedundefinedjs run try { {{{{{{{{{{{{ } catch (err) { alert("The engine can't understand this code, it's invalid"); }undefinedundefined
The JavaScript engine first reads the code, and then runs it. The errors that occur on the reading phase are called "parse-time" errors and are unrecoverable (from inside that code). That's because the engine can't understand the code.
undefinedundefinedSo, undefinedundefinedtry...catch can only handle
errors that occur in valid code. Such errors are called "runtime errors" or, sometimes, "exceptions".
undefinedundefined
undefinedundefinedwarn header="try…catchundefinedundefinedworks synchronously" If an exception happens in "scheduled" code, like insetTimeoutundefinedundefined, thentry…catch`
won't catch it:undefinedundefined
undefinedundefinedjs run try { setTimeout(function() { noSuchVariable; // script will die here }, 1000); } catch (err) { alert( "won't work" ); }undefinedundefined
That's because the function itself is executed later, when the engine has already left the
undefinedundefinedtry...catch construct.undefinedundefined
To catch an exception
inside a scheduled function, undefinedundefinedtry...catch must be inside that function:
undefinedundefinedjs run setTimeout(function() { try { noSuchVariable; // try...catch handles the error! } catch { alert( "error is caught here!" ); } }, 1000);
undefinedundefined
When an error
occurs, JavaScript generates an object containing the details about it. The object is then passed as an argument to
undefinedundefinedcatch:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedtryundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedcatch (err) undefinedundefined{undefinedundefined// <-- the "error object", could use another word instead of errundefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
For all built-in errors, the error object has two main properties:
undefinedundefinednameundefinedundefined"ReferenceError".
undefinedundefinedmessageundefinedundefinedThere are other non-standard properties available in most environments. One of most widely used and supported is:
undefinedundefinedstackundefinedundefinedFor instance:
undefinedundefinedrun untrusted try { undefinedundefined! lalala; // error, variable is not defined! undefinedundefined/! } catch (err) { alert(err.name); // ReferenceError alert(err.message); // lalala is not defined alert(err.stack); // ReferenceError: lalala is not defined at (…call stack)undefinedundefined
undefinedundefined// Can also show an error as a whole // The error is converted to string as "name: message" alert(err); // ReferenceError: lalala is not defined }
undefinedundefined[recent browser=new]
undefinedundefinedIf we don't need error details, undefinedundefinedcatch may omit
it:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedtryundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedcatchundefinedundefined{undefinedundefined// <-- without (err)undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Let's explore a real-life use
case of undefinedundefinedtry...catch.undefinedundefined
As we already know, JavaScript supports the undefinedundefinedJSON.parse(str) method to read JSON-encoded values.undefinedundefined
undefinedundefinedUsually it's used to decode data received over the network, from the server or another source.
undefinedundefinedWe receive it and call undefinedundefinedJSON.parse
like this:undefinedundefined
run let json = ‘{"name":"John", "age": 30}'''; // data from the server
undefinedundefinedundefinedundefined! let user = JSON.parse(json); // convert the text representation to JS object undefinedundefined/!undefinedundefined
undefinedundefined// now user is an object with properties from the string alert( user.name ); // John alert( user.age ); // 30
undefinedundefinedYou can find more detailed information about JSON in the undefinedundefinedinfo:json chapter.undefinedundefined
undefinedundefinedundefinedundefinedIf
undefinedundefinedjson is malformed, undefinedundefinedJSON.parse generates an error, so
the script "dies".undefinedundefinedundefinedundefined
Should we be satisfied with that? Of course not!
undefinedundefinedThis way, if something's wrong with the data, the visitor will never know that (unless they open the developer console). And people really don't like when something "just dies" without any error message.
undefinedundefinedLet's use undefinedundefinedtry...catch to handle the
error:undefinedundefined
run let json = "{ bad json }";
undefinedundefinedtry {
undefinedundefinedundefinedundefined! let user = JSON.parse(json); // <- when an error occurs… undefinedundefined/! alert( user.name ); // doesn't workundefinedundefined
undefinedundefined} catch (err) { undefinedundefined! // …the execution jumps here alert( "Our apologies, the data has errors, we'll try to request it one more time." ); alert( err.name ); alert( err.message ); undefinedundefined/! } undefinedundefined
undefinedundefinedHere we use the undefinedundefinedcatch block only to show the
message, but we can do much more: send a new network request, suggest an alternative to the visitor, send information
about the error to a logging facility, … . All much better than just dying.undefinedundefined
What if
undefinedundefinedjson is syntactically correct, but doesn't have a required
undefinedundefinedname property?undefinedundefined
Like this:
undefinedundefinedrun let json = ‘{ "age": 30 }'''; // incomplete data
undefinedundefinedtry {
undefinedundefinedlet user = JSON.parse(json); // <- no errors undefinedundefined! alert( user.name ); // no name! undefinedundefined/!undefinedundefined
undefinedundefined} catch (err) { alert( "doesn't execute" ); }
undefinedundefinedHere undefinedundefinedJSON.parse runs normally, but the absence of
undefinedundefinedname is actually an error for us.undefinedundefined
To unify
error handling, we'll use the undefinedundefinedthrow operator.undefinedundefined
The undefinedundefinedthrow operator
generates an error.undefinedundefined
The syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedthrowundefinedundefined<error objectundefinedundefined>undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Technically, we can use anything as an error object. That may be even a primitive, like a
number or a string, but it's better to use objects, preferably with undefinedundefinedname and
undefinedundefinedmessage properties (to stay somewhat compatible with built-in
errors).undefinedundefined
JavaScript has many built-in constructors for standard errors:
undefinedundefinedError, undefinedundefinedSyntaxError,
undefinedundefinedReferenceError, undefinedundefinedTypeError and others. We can use them to
create error objects as well.undefinedundefined
Their syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet error undefinedundefined=undefinedundefinednewundefinedundefinedError(message)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined// orundefinedundefinedundefinedundefinedundefinedundefinedlet error undefinedundefined=undefinedundefinednewundefinedundefinedSyntaxError(message)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedlet error undefinedundefined=undefinedundefinednewundefinedundefinedReferenceError(message)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
For built-in errors (not for any objects, just for errors), the
undefinedundefinedname property is exactly the name of the constructor. And
undefinedundefinedmessage is taken from the argument.undefinedundefined
For instance:
undefinedundefinedrun let error = new Error("Things happen o_O");
undefinedundefinedalert(error.name); // Error alert(error.message); // Things happen o_O
undefinedundefinedLet's see what kind of error undefinedundefinedJSON.parse
generates:undefinedundefined
undefinedundefinedjs run try { JSON.parse("{ bad json o_O }"); } catch (err) { *!* alert(err.name); // SyntaxError */!* alert(err.message); // Unexpected token b in JSON at position 2 }undefinedundefined
As we can see, that's a undefinedundefinedSyntaxError.undefinedundefined
And in our case, the absence of undefinedundefinedname is an error, as users must have
a undefinedundefinedname.undefinedundefined
So let's throw it:
undefinedundefinedrun let json = ‘{ "age": 30 }'''; // incomplete data
undefinedundefinedtry {
undefinedundefinedlet user = JSON.parse(json); // <- no errors
undefinedundefinedif (!user.name) { undefinedundefined! throw new SyntaxError("Incomplete data: no name"); // (undefinedundefined) /!* }undefinedundefined
undefinedundefinedalert( user.name );
undefinedundefined} catch (err) { alert( "JSON Error:" + err.message ); // JSON Error: Incomplete data: no name }
undefinedundefinedIn the line undefinedundefined(*), the undefinedundefinedthrow
operator generates a undefinedundefinedSyntaxError with the given undefinedundefinedmessage,
the same way as JavaScript would generate it itself. The execution of undefinedundefinedtry immediately
stops and the control flow jumps into undefinedundefinedcatch.undefinedundefined
Now undefinedundefinedcatch became a single place for all error handling: both for
undefinedundefinedJSON.parse and other cases.undefinedundefined
In the example above we use undefinedundefinedtry...catch to handle
incorrect data. But is it possible that undefinedundefinedanother unexpected error occurs within the
undefinedundefinedtry {...} block? Like a programming error (variable is not defined) or something else,
not just this "incorrect data" thing.undefinedundefined
For example:
undefinedundefinedrun let json = ‘{ "age": 30 }'''; // incomplete data
undefinedundefinedtry { user = JSON.parse(json); // <- forgot to put "let" before user
undefinedundefined// … } catch (err) { alert("JSON Error:" + err); // JSON Error: ReferenceError: user is not defined // (no JSON Error actually) }
undefinedundefinedOf course, everything's possible! Programmers do make mistakes. Even in open-source utilities used by millions for decades - suddenly a bug may be discovered that leads to terrible hacks.
undefinedundefined
In our case, undefinedundefinedtry...catch is placed to catch "incorrect data" errors. But by its nature,
undefinedundefinedcatch gets undefinedundefinedall errors from
undefinedundefinedtry. Here it gets an unexpected error, but still shows the same
undefinedundefined"JSON Error" message. That's wrong and also makes the code more difficult to
debug.undefinedundefined
To avoid such problems, we can employ the "rethrowing" technique. The rule is simple:
undefinedundefinedundefinedundefinedCatch should only process errors that it knows and "rethrow" all others.undefinedundefined
undefinedundefinedThe "rethrowing" technique can be explained in more detail as:
undefinedundefinedcatch (err) {...} block we analyze the error object
undefinedundefinederr.undefinedundefinedthrow err.undefinedundefined
Usually, we can check the error type using the undefinedundefinedinstanceof operator:undefinedundefined
undefinedundefinedjs run try { user = { /*...*/ }; } catch (err) { *!* if (err instanceof ReferenceError) { */!* alert('ReferenceError'); // "ReferenceError" for accessing an undefined variable } }undefinedundefined
We can also get the error class name from undefinedundefinederr.name property. All
native errors have it. Another option is to read
undefinedundefinederr.constructor.name.undefinedundefined
In the code below, we
use rethrowing so that undefinedundefinedcatch only handles
undefinedundefinedSyntaxError:undefinedundefined
run let json = ‘{ "age": 30 }'''; // incomplete data try {
undefinedundefinedlet user = JSON.parse(json);
undefinedundefinedif (!user.name) { throw new SyntaxError("Incomplete data: no name"); }
undefinedundefinedundefinedundefined! blabla(); // unexpected error undefinedundefined/!undefinedundefined
undefinedundefinedalert( user.name );
undefinedundefined} catch (err) {
undefinedundefinedundefinedundefined! if (err instanceof SyntaxError) { alert( "JSON Error:" + err.message ); } else { throw err; // rethrow (undefinedundefined) } /!*undefinedundefined
undefinedundefined}
undefinedundefinedThe error throwing on line undefinedundefined(*) from inside
undefinedundefinedcatch block "falls out" of undefinedundefinedtry...catch and can be either
caught by an outer undefinedundefinedtry...catch construct (if it exists), or it kills the
script.undefinedundefined
So the undefinedundefinedcatch block actually handles
only errors that it knows how to deal with and "skips" all others.undefinedundefined
The
example below demonstrates how such errors can be caught by one more level of
undefinedundefinedtry...catch:undefinedundefined
run function readData() { let json = ‘{ "age": 30 }''';
undefinedundefinedtry { // … undefinedundefined! blabla(); // error! undefinedundefined/! } catch (err) { // … if (!(err instanceof SyntaxError)) { undefinedundefined! throw err; // rethrow (don't know how to deal with it) undefinedundefined/! } } }undefinedundefined
undefinedundefinedtry { readData(); } catch (err) { undefinedundefined! alert( "External catch got:" + err ); // caught it! undefinedundefined/! } undefinedundefined
undefinedundefinedHere undefinedundefinedreadData only knows how to handle
undefinedundefinedSyntaxError, while the outer undefinedundefinedtry...catch knows how to
handle everything.undefinedundefined
Wait, that's not all.
undefinedundefinedThe undefinedundefinedtry...catch
construct may have one more code clause: undefinedundefinedfinally.undefinedundefined
If it exists, it runs in all cases:
undefinedundefinedtry, if there were no errors,undefinedundefinedcatch, if there were errors.undefinedundefinedThe extended syntax looks like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedtryundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined ... try to execute the code ...undefinedundefinedundefinedundefinedundefinedundefined} undefinedundefined*undefinedundefined!undefinedundefined*undefinedundefinedcatchundefinedundefined*undefinedundefined/undefinedundefined!* (err) undefinedundefined{undefinedundefinedundefinedundefined ... undefinedundefinedhandleundefinedundefinederrors ...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefined*!*undefinedundefinedfinallyundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined ... execute always ...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Try running this code:
undefinedundefined
undefinedundefinedjs run try { alert( 'try' ); if (confirm('Make an error?')) BAD_CODE(); } catch (err) { alert( 'catch' ); } finally { alert( 'finally' ); }undefinedundefined
The code has two ways of execution:
undefinedundefinedtry -> catch -> finally.undefinedundefinedtry -> finally.undefinedundefinedThe undefinedundefinedfinally clause is often used when we start doing something and
want to finalize it in any case of outcome.undefinedundefined
For instance, we want to measure
the time that a Fibonacci numbers function undefinedundefinedfib(n) takes. Naturally, we can start
measuring before it runs and finish afterwards. But what if there's an error during the function call? In particular,
the implementation of undefinedundefinedfib(n) in the code below returns an error for negative or
non-integer numbers.undefinedundefined
The undefinedundefinedfinally clause is a
great place to finish the measurements no matter what.undefinedundefined
Here
undefinedundefinedfinally guarantees that the time will be measured correctly in both situations - in
case of a successful execution of undefinedundefinedfib and in case of an error in it:undefinedundefined
run let num = +prompt("Enter a positive integer number?", 35)
undefinedundefinedlet diff, result;
undefinedundefinedfunction fib(n) { if (n < 0 || Math.trunc(n) != n) { throw new Error("Must not be negative, and also an integer."); } return n <= 1 ? n : fib(n - 1) + fib(n - 2); }
undefinedundefinedlet start = Date.now();
undefinedundefinedtry { result = fib(num); } catch (err) { result = 0; undefinedundefined! } finally { diff = Date.now() - start; } undefinedundefined/!undefinedundefined
undefinedundefinedalert(result || "error occurred");
undefinedundefinedalert(
undefinedundefinedexecution took ${diff}ms );
undefinedundefined
You can check by running the code with entering
undefinedundefined35 into undefinedundefinedprompt - it executes normally,
undefinedundefinedfinally after undefinedundefinedtry. And then enter
undefinedundefined-1 - there will be an immediate error, and the execution will take
undefinedundefined0ms. Both measurements are done correctly.undefinedundefined
In
other words, the function may finish with undefinedundefinedreturn or
undefinedundefinedthrow, that doesn't matter. The undefinedundefinedfinally clause executes
in both cases.undefinedundefined
``undefinedundefinedsmart header="Variables are local insidetry…catch…finallyundefinedundefined" Please note thatresultundefinedundefinedanddiffundefinedundefinedvariables in the code above are declared *before*try…catch`.undefinedundefined
Otherwise, if we declared undefinedundefinedlet in
undefinedundefinedtry block, it would only be visible inside of it.
undefinedundefined
undefinedundefinedsmart header="finallyundefinedundefinedandreturnundefinedundefined" Thefinallyundefinedundefinedclause works for *any* exit fromtry…catchundefinedundefined. That includes an explicitreturn`.undefinedundefined
In the example below, there's a undefinedundefinedreturn in
undefinedundefinedtry. In this case, undefinedundefinedfinally is executed just before the
control returns to the outer code.undefinedundefined
run function func() {
undefinedundefinedtry { undefinedundefined! return 1; undefinedundefined/!undefinedundefined
undefinedundefined} catch (err) { /* … undefinedundefined/ } finally { !undefinedundefined alert( ‘finally' ); /!* } }undefinedundefined
undefinedundefinedalert( func() ); // first works alert from finally, and then this one
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefinedsmart header="try…finally`"undefinedundefined
The
undefinedundefinedtry...finally construct, without undefinedundefinedcatch clause, is also
useful. We apply it when we don't want to handle errors here (let them fall through), but want to be sure that
processes that we started are finalized.undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedfunc() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// start doing something that needs completion (like measurements)undefinedundefinedundefinedundefinedundefinedundefinedtryundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedfinallyundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// complete that thing even if all diesundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In the code above, an error inside undefinedundefinedtry always falls out,
because there's no undefinedundefinedcatch. But undefinedundefinedfinally works before the
execution flow leaves the function.
undefinedundefined
undefinedundefinedwarn header="Environment-specific" The information from this section is not a part of the core JavaScript.undefinedundefined
Let's imagine we've got a fatal error outside of undefinedundefinedtry...catch,
and the script died. Like a programming error or some other terrible thing.undefinedundefined
Is there a way to react on such occurrences? We may want to log the error, show something to the user (normally they don't see error messages), etc.
undefinedundefinedThere is none in the specification, but environments usually
provide it, because it's really useful. For instance, Node.js has undefinedundefinedundefinedundefinedprocess.on("uncaughtException")undefinedundefined
for that. And in the browser we can assign a function to the special undefinedundefinedwindow.onerror property, that will run in case of an uncaught
error.undefinedundefined
The syntax:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedwindow.undefinedundefinedonerrorundefinedundefined=undefinedundefinedfunction(messageundefinedundefined, urlundefinedundefined, lineundefinedundefined, colundefinedundefined, error) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
messageundefinedundefinedurlundefinedundefinedline, undefinedundefinedcolundefinedundefined
errorundefinedundefinedFor instance:
undefinedundefined
``undefinedundefinedhtml run untrusted refresh height=1 <script> *!* window.onerror = function(message, url, line, col, error) { alert(${message}At
undefinedundefinedundefinedundefinedlundefinedundefinediundefinedundefinednundefinedundefinede:undefinedundefined{col}
of ${url}`); }; undefinedundefined/!undefinedundefined
function readData() { badFunc(); // Whoops, something went wrong! }
readData(); undefinedundefinedundefinedundefinedundefinedundefined
The role of the global handler undefinedundefinedwindow.onerror is usually not to
recover the script execution - that's probably impossible in case of programming errors, but to send the error message
to developers.undefinedundefined
There are also web-services that provide error-logging for such cases, like undefinedundefinedhttps://errorception.com or undefinedundefinedhttp://www.muscula.com.undefinedundefined
undefinedundefinedThey work like this:
undefinedundefinedwindow.onerror function.undefinedundefinedThe undefinedundefinedtry...catch construct allows to
handle runtime errors. It literally allows to "try" running the code and "catch" errors that may occur in
it.undefinedundefined
The syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedtryundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// run this codeundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedcatch (err) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// if an error happened, then jump hereundefinedundefinedundefinedundefinedundefinedundefined// err is the error objectundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedfinallyundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// do in any case after try/catchundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
There may be no undefinedundefinedcatch section or no
undefinedundefinedfinally, so shorter constructs undefinedundefinedtry...catch and
undefinedundefinedtry...finally are also valid.undefinedundefined
Error objects have following properties:
undefinedundefinedmessage - the
human-readable error message.undefinedundefinedname - the
string with error name (error constructor name).undefinedundefinedstack (non-standard, but well-supported) - the stack at the moment of error
creation.undefinedundefinedIf an error object is not needed, we can
omit it by using undefinedundefinedcatch { instead of
undefinedundefinedcatch (err) {.undefinedundefined
We can also generate our own
errors using the undefinedundefinedthrow operator. Technically, the argument of
undefinedundefinedthrow can be anything, but usually it's an error object inheriting from the built-in
undefinedundefinedError class. More on extending errors in the next chapter.undefinedundefined
undefinedundefinedRethrowing is a very important pattern of error handling: a
undefinedundefinedcatch block usually expects and knows how to handle the particular error type, so it
should rethrow errors it doesn't know.undefinedundefined
Even if we don't have
undefinedundefinedtry...catch, most environments allow us to setup a "global" error handler to catch
errors that "fall out". In-browser, that's undefinedundefinedwindow.onerror.undefinedundefined
When we
develop something, we often need our own error classes to reflect specific things that may go wrong in our tasks. For
errors in network operations we may need undefinedundefinedHttpError, for database operations
undefinedundefinedDbError, for searching operations undefinedundefinedNotFoundError and so
on.undefinedundefined
Our errors should support basic error properties like
undefinedundefinedmessage, undefinedundefinedname and, preferably,
undefinedundefinedstack. But they also may have other properties of their own,
e.g. undefinedundefinedHttpError objects may have a undefinedundefinedstatusCode property
with a value like undefinedundefined404 or undefinedundefined403 or
undefinedundefined500.undefinedundefined
JavaScript allows to use
undefinedundefinedthrow with any argument, so technically our custom error classes don't need to inherit
from undefinedundefinedError. But if we inherit, then it becomes possible to use
undefinedundefinedobj instanceof Error to identify error objects. So it's better to inherit from
it.undefinedundefined
As the application grows, our own errors naturally form a hierarchy. For
instance, undefinedundefinedHttpTimeoutError may inherit from undefinedundefinedHttpError,
and so on.undefinedundefined
As
an example, let's consider a function undefinedundefinedreadUser(json) that should read JSON with user
data.undefinedundefined
Here's an example of how a valid undefinedundefinedjson
may look:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet json undefinedundefined=undefinedundefined`{ "name": "John", "age": 30 }`undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Internally, we'll use undefinedundefinedJSON.parse. If it receives malformed
undefinedundefinedjson, then it throws undefinedundefinedSyntaxError. But even if
undefinedundefinedjson is syntactically correct, that doesn't mean that it's a valid user, right? It may
miss the necessary data. For instance, it may not have undefinedundefinedname and
undefinedundefinedage properties that are essential for our users.undefinedundefined
Our function undefinedundefinedreadUser(json) will not only read JSON, but check
("validate") the data. If there are no required fields, or the format is wrong, then that's an error. And that's not a
undefinedundefinedSyntaxError, because the data is syntactically correct, but another kind of error.
We'll call it undefinedundefinedValidationError and create a class for it. An error of that kind should
also carry the information about the offending field.undefinedundefined
Our
undefinedundefinedValidationError class should inherit from the built-in
undefinedundefinedError class.undefinedundefined
That class is built-in, but here's its approximate code so we can understand what we're extending:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// The "pseudocode" for the built-in Error class defined by JavaScript itselfundefinedundefinedundefinedundefinedundefinedundefinedclass Error undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedconstructor(message) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedthis.undefinedundefinedmessageundefinedundefined= messageundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedthis.undefinedundefinednameundefinedundefined=undefinedundefined"Error"undefinedundefined;undefinedundefined// (different names for different built-in error classes)undefinedundefinedundefinedundefinedundefinedundefinedthis.undefinedundefinedstackundefinedundefined=undefinedundefined<call stackundefinedundefined>;undefinedundefined// non-standard, but most environments support itundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Now let's inherit undefinedundefinedValidationError from it and try it in
action:undefinedundefined
run untrusted undefinedundefined! class ValidationError extends Error { undefinedundefined/! constructor(message) { super(message); // (1) this.name = "ValidationError"; // (2) } }undefinedundefined
undefinedundefinedfunction test() { throw new ValidationError("Whoops!"); }
undefinedundefinedtry { test(); } catch(err) { alert(err.message); // Whoops! alert(err.name); // ValidationError alert(err.stack); // a list of nested calls with line numbers for each }
undefinedundefinedPlease note: in the line undefinedundefined(1) we call the parent constructor.
JavaScript requires us to call undefinedundefinedsuper in the child constructor, so that's obligatory.
The parent constructor sets the undefinedundefinedmessage property.undefinedundefined
The parent constructor also sets the undefinedundefinedname property to
undefinedundefined"Error", so in the line undefinedundefined(2) we reset it to the right
value.undefinedundefined
Let's try to use it in
undefinedundefinedreadUser(json):undefinedundefined
run class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; } }
undefinedundefined// Usage function readUser(json) { let user = JSON.parse(json);
undefinedundefinedif (!user.age) { throw new ValidationError("No field: age"); } if (!user.name) { throw new ValidationError("No field: name"); }
undefinedundefinedreturn user; }
undefinedundefined// Working example with try..catch
undefinedundefinedtry { let user = readUser(‘{ "age": 25 }'''); } catch (err) { if (err instanceof ValidationError) { undefinedundefined! alert("Invalid data:" + err.message); // Invalid data: No field: name undefinedundefined/! } else if (err instanceof SyntaxError) { // (*) alert("JSON Syntax Error:" + err.message); } else { throw err; // unknown error, rethrow it (**) } } undefinedundefined
undefinedundefinedThe undefinedundefinedtry..catch block in the code above
handles both our undefinedundefinedValidationError and the built-in
undefinedundefinedSyntaxError from undefinedundefinedJSON.parse.undefinedundefined
Please take a look at how we use undefinedundefinedinstanceof to check for the
specific error type in the line undefinedundefined(*).undefinedundefined
We could
also look at undefinedundefinederr.name, like this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined// instead of (err instanceof SyntaxError)undefinedundefinedundefinedundefined} undefinedundefinedelseundefinedundefinedif (undefinedundefinederr.undefinedundefinednameundefinedundefined==undefinedundefined"SyntaxError") undefinedundefined{undefinedundefined// (*)undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The undefinedundefinedinstanceof version is much better, because in the future
we are going to extend undefinedundefinedValidationError, make subtypes of it, like
undefinedundefinedPropertyRequiredError. And undefinedundefinedinstanceof check will
continue to work for new inheriting classes. So that's future-proof.undefinedundefined
Also
it's important that if undefinedundefinedcatch meets an unknown error, then it rethrows it in the line
undefinedundefined(**). The undefinedundefinedcatch block only knows how to handle
validation and syntax errors, other kinds (due to a typo in the code or other unknown ones) should fall
through.undefinedundefined
The undefinedundefinedValidationError class is very generic. Many things may go wrong.
The property may be absent or it may be in a wrong format (like a string value for
undefinedundefinedage). Let's make a more concrete class
undefinedundefinedPropertyRequiredError, exactly for absent properties. It will carry additional
information about the property that's missing.undefinedundefined
run class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; } }
undefinedundefinedundefinedundefined! class PropertyRequiredError extends ValidationError { constructor(property) { super("No property:" + property); this.name = "PropertyRequiredError"; this.property = property; } } undefinedundefined/!undefinedundefined
undefinedundefined// Usage function readUser(json) { let user = JSON.parse(json);
undefinedundefinedif (!user.age) { throw new PropertyRequiredError("age"); } if (!user.name) { throw new PropertyRequiredError("name"); }
undefinedundefinedreturn user; }
undefinedundefined// Working example with try..catch
undefinedundefinedtry { let user = readUser(‘{ "age": 25 }'''); } catch (err) { if (err instanceof ValidationError) { undefinedundefined! alert("Invalid data:" + err.message); // Invalid data: No property: name alert(err.name); // PropertyRequiredError alert(err.property); // name undefinedundefined/! } else if (err instanceof SyntaxError) { alert("JSON Syntax Error:" + err.message); } else { throw err; // unknown error, rethrow it } } undefinedundefined
undefinedundefinedThe new class undefinedundefinedPropertyRequiredError is easy
to use: we only need to pass the property name: undefinedundefinednew PropertyRequiredError(property).
The human-readable undefinedundefinedmessage is generated by the constructor.undefinedundefined
Please note that undefinedundefinedthis.name in
undefinedundefinedPropertyRequiredError constructor is again assigned manually. That may become a bit
tedious - to assign undefinedundefinedthis.name = <class name> in every custom error class. We can
avoid it by making our own "basic error" class that assigns
undefinedundefinedthis.name = this.constructor.name. And then inherit all our custom errors from
it.undefinedundefined
Let's call it undefinedundefinedMyError.undefinedundefined
Here's the code with undefinedundefinedMyError and other custom error classes,
simplified:undefinedundefined
run class MyError extends Error { constructor(message) { super(message); undefinedundefined! this.name = this.constructor.name; undefinedundefined/! } }undefinedundefined
undefinedundefinedclass ValidationError extends MyError { }
undefinedundefinedclass PropertyRequiredError extends ValidationError { constructor(property) { super("No property:" + property); this.property = property; } }
undefinedundefined// name is correct alert( new PropertyRequiredError("field").name ); // PropertyRequiredError
undefinedundefinedNow custom errors are much shorter, especially undefinedundefinedValidationError,
as we got rid of the undefinedundefined"this.name = ..." line in the constructor.undefinedundefined
The purpose of the function
undefinedundefinedreadUser in the code above is "to read the user data". There may occur different kinds
of errors in the process. Right now we have undefinedundefinedSyntaxError and
undefinedundefinedValidationError, but in the future undefinedundefinedreadUser function may
grow and probably generate other kinds of errors.undefinedundefined
The code which calls
undefinedundefinedreadUser should handle these errors. Right now it uses multiple
undefinedundefinedifs in the undefinedundefinedcatch block, that check the class and handle
known errors and rethrow the unknown ones.undefinedundefined
The scheme is like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedtryundefinedundefined{undefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefinedreadUser() undefinedundefined// the potential error sourceundefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined}undefinedundefinedcatch (err) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (err undefinedundefinedinstanceof ValidationError) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// handle validation errorsundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedelseundefinedundefinedif (err undefinedundefinedinstanceof SyntaxError) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// handle syntax errorsundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedelseundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedthrow errundefinedundefined;undefinedundefined// unknown error, rethrow itundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In the code above we can see two types of errors, but there can be more.
undefinedundefinedIf the undefinedundefinedreadUser function generates several kinds of errors, then we
should ask ourselves: do we really want to check for all error types one-by-one every time?undefinedundefined
Often the answer is "No": we'd like to be "one level above all that". We just want to know if there was a "data reading error" - why exactly it happened is often irrelevant (the error message describes it). Or, even better, we'd like to have a way to get the error details, but only if we need to.
undefinedundefinedThe technique that we describe here is called "wrapping exceptions".
undefinedundefinedReadError to represent a generic "data reading"
error.undefinedundefinedreadUser will catch
data reading errors that occur inside it, such as undefinedundefinedValidationError and
undefinedundefinedSyntaxError, and generate a undefinedundefinedReadError
instead.undefinedundefinedReadError object will keep
the reference to the original error in its undefinedundefinedcause property.undefinedundefinedThen the code that calls undefinedundefinedreadUser will only
have to check for undefinedundefinedReadError, not for every kind of data reading errors. And if it needs
more details of an error, it can check its undefinedundefinedcause property.undefinedundefined
Here's the code that defines undefinedundefinedReadError and demonstrates its use in
undefinedundefinedreadUser and undefinedundefinedtry..catch:undefinedundefined
run class ReadError extends Error { constructor(message, cause) { super(message); this.cause = cause; this.name = ‘ReadError'; } }
undefinedundefinedclass ValidationError extends Error { /undefinedundefined…/ } class PropertyRequiredError extends ValidationError { /* … */ }undefinedundefined
undefinedundefinedfunction validateUser(user) { if (!user.age) { throw new PropertyRequiredError("age"); }
undefinedundefinedif (!user.name) { throw new PropertyRequiredError("name"); } }
undefinedundefinedfunction readUser(json) { let user;
undefinedundefinedtry { user = JSON.parse(json); } catch (err) { undefinedundefined! if (err instanceof SyntaxError) { throw new ReadError("Syntax Error", err); } else { throw err; } undefinedundefined/! }undefinedundefined
undefinedundefinedtry { validateUser(user); } catch (err) { undefinedundefined! if (err instanceof ValidationError) { throw new ReadError("Validation Error", err); } else { throw err; } undefinedundefined/! }undefinedundefined
undefinedundefined}
undefinedundefinedtry { readUser(‘{bad json}'''); } catch (e) { if (e instanceof ReadError) { undefinedundefined! alert(e); // Original error: SyntaxError: Unexpected token b in JSON at position 1 alert("Original error:" + e.cause); undefinedundefined/! } else { throw e; } } undefinedundefined
undefinedundefinedIn the code above, undefinedundefinedreadUser works exactly as
described - catches syntax and validation errors and throws undefinedundefinedReadError errors instead
(unknown errors are rethrown as usual).undefinedundefined
So the outer code checks
undefinedundefinedinstanceof ReadError and that's it. No need to list all possible error
types.undefinedundefined
The approach is called "wrapping exceptions", because we take "low
level" exceptions and "wrap" them into undefinedundefinedReadError that is more abstract. It is widely
used in object-oriented programming.undefinedundefined
Error and other built-in
error classes normally. We just need to take care of the undefinedundefinedname property and don't
forget to call undefinedundefinedsuper.undefinedundefinedinstanceof to check for particular errors. It also works with inheritance. But
sometimes we have an error object coming from a 3rd-party library and there's no easy way to get its class. Then
undefinedundefinedname property can be used for such checks.undefinedundefinederr.cause in the examples above, but that's not strictly required.undefinedundefined
warn header="We use browser methods in examples here" To demonstrate the use of callbacks, promises and other abstract concepts, we'll be using some browser methods: specifically, loading scripts and performing simple document manipulations.
undefinedundefinedIf you're not familiar with these methods, and their usage in the examples is confusing, you may want to read a few chapters from the undefinedundefinednext part of the tutorial.undefinedundefined
undefinedundefinedAlthough, we'll try to make things clear anyway. There won't be anything really complex browser-wise.
undefinedundefinedMany functions are provided by JavaScript host environments that allow you to schedule undefinedundefinedasynchronous actions. In other words, actions that we initiate now, but they finish later.undefinedundefined
undefinedundefinedFor instance, one such function is the
undefinedundefinedsetTimeout function.undefinedundefined
There are other real-world examples of asynchronous actions, e.g. loading scripts and modules (we'll cover them in later chapters).
undefinedundefinedTake a look at the function undefinedundefinedloadScript(src), that loads a script
with the given undefinedundefinedsrc:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedloadScript(src) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// creates a <script> tag and append it to the pageundefinedundefinedundefinedundefinedundefinedundefined// this causes the script with given src to start loading and run when completeundefinedundefinedundefinedundefinedundefinedundefinedlet script undefinedundefined=undefinedundefineddocument.undefinedundefinedcreateElement(undefinedundefined'script')undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedscript.undefinedundefinedsrcundefinedundefined= srcundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefineddocument.undefinedundefinedhead.undefinedundefinedappend(script)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
It appends to the document the new, dynamically created, tag
undefinedundefined<script src="…"> with given undefinedundefinedsrc. The browser
automatically starts loading it and executes when complete.undefinedundefined
We can use this function like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// load and execute the script at the given pathundefinedundefinedundefinedundefinedundefinedundefinedloadScript(undefinedundefined'/my/script.js')undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The script is executed "asynchronously", as it starts loading now, but runs later, when the function has already finished.
undefinedundefinedIf there's any code below
undefinedundefinedloadScript(…), it doesn't wait until the script loading finishes.undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedloadScript(undefinedundefined'/my/script.js')undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined// the code below loadScriptundefinedundefinedundefinedundefinedundefinedundefined// doesn't wait for the script loading to finishundefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Let's say we need to use the new script as soon as it loads. It declares new functions, and we want to run them.
undefinedundefinedBut if we do that immediately after the
undefinedundefinedloadScript(…) call, that wouldn't work:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedloadScript(undefinedundefined'/my/script.js')undefinedundefined;undefinedundefined// the script has "function newFunction() {…}"undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinednewFunction()undefinedundefined;undefinedundefined// no such function!undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Naturally, the browser probably didn't have time to load the script. As of now, the
undefinedundefinedloadScript function doesn't provide a way to track the load completion. The script
loads and eventually runs, that's all. But we'd like to know when it happens, to use new functions and variables from
that script.undefinedundefined
Let's add a undefinedundefinedcallback function as
a second argument to undefinedundefinedloadScript that should execute when the script
loads:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedloadScript(srcundefinedundefined,undefinedundefined*!*callbackundefinedundefined*undefinedundefined/!undefinedundefined*)undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined let script = document.createElementundefinedundefined(undefinedundefined'script'undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined script.src = src;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined script.onload = undefinedundefined()undefinedundefined => callbackundefinedundefined(undefinedundefinedscriptundefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/undefinedundefined!*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefineddocument.undefinedundefinedhead.undefinedundefinedappend(script)undefinedundefined;undefinedundefinedundefinedundefined}undefinedundefinedundefinedundefined
undefinedundefined
Now if we want to call new functions from the script, we should write that in the callback:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedloadScript(undefinedundefined'/my/script.js'undefinedundefined,undefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// the callback runs after the script is loadedundefinedundefinedundefinedundefinedundefinedundefinednewFunction()undefinedundefined;undefinedundefined// so now it worksundefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That's the idea: the second argument is a function (usually anonymous) that runs when the action is completed.
undefinedundefinedHere's a runnable example with a real script:
undefinedundefinedrun function loadScript(src, callback) { let script = document.createElement(‘script'); script.src = src; script.onload = () => callback(script); document.head.append(script); }
undefinedundefined
undefinedundefined! loadScript(‘https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script
=> { alert(undefinedundefinedCool, the script ${script.src} is loaded); alert( _ ); // function
declared in the loaded script }); undefinedundefined/!
undefinedundefined
That's called a "callback-based" style of asynchronous programming. A
function that does something asynchronously should provide a undefinedundefinedcallback argument where we
put the function to run after it's complete.undefinedundefined
Here we did it in
undefinedundefinedloadScript, but of course it's a general approach.undefinedundefined
How can we load two scripts sequentially: the first one, and then the second one after it?
undefinedundefinedThe natural solution
would be to put the second undefinedundefinedloadScript call inside the callback, like
this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedloadScript(undefinedundefined'/my/script.js'undefinedundefined,undefinedundefinedfunction(script) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`Cool, the undefinedundefined${undefinedundefinedscript.undefinedundefinedsrcundefinedundefined}undefinedundefined is loaded, let's load one more`)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedloadScript(undefinedundefined'/my/script2.js'undefinedundefined,undefinedundefinedfunction(script) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`Cool, the second script is loaded`)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
After the outer undefinedundefinedloadScript is complete, the callback initiates
the inner one.undefinedundefined
What if we want one more script…?
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedloadScript(undefinedundefined'/my/script.js'undefinedundefined,undefinedundefinedfunction(script) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedloadScript(undefinedundefined'/my/script2.js'undefinedundefined,undefinedundefinedfunction(script) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedloadScript(undefinedundefined'/my/script3.js'undefinedundefined,undefinedundefinedfunction(script) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...continue after all scripts are loadedundefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined }undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
So, every new action is inside a callback. That's fine for few actions, but not good for many, so we'll see other variants soon.
undefinedundefinedIn the above examples we didn't consider errors. What if the script loading fails? Our callback should be able to react on that.
undefinedundefinedHere's an improved version of
undefinedundefinedloadScript that tracks loading errors:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedloadScript(srcundefinedundefined, callback) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet script undefinedundefined=undefinedundefineddocument.undefinedundefinedcreateElement(undefinedundefined'script')undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedscript.undefinedundefinedsrcundefinedundefined= srcundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedscript.undefinedundefinedonloadundefinedundefined= () undefinedundefined=>undefinedundefinedcallback(undefinedundefinednullundefinedundefined, script)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedscript.undefinedundefinedonerrorundefinedundefined= () undefinedundefined=>undefinedundefinedcallback(undefinedundefinednewundefinedundefinedError(undefinedundefined`Script load error for undefinedundefined${srcundefinedundefined}undefinedundefined`))undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined document.head.appendundefinedundefined(undefinedundefinedscriptundefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
It calls undefinedundefinedcallback(null, script) for successful load and
undefinedundefinedcallback(error) otherwise.undefinedundefined
The usage:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedloadScript(undefinedundefined'/my/script.js'undefinedundefined,undefinedundefinedfunction(errorundefinedundefined, script) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (error) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// handle errorundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedelseundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// script loaded successfullyundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Once again, the recipe that we used for undefinedundefinedloadScript is actually
quite common. It's called the "error-first callback" style.undefinedundefined
The convention
is: 1. The first argument of the undefinedundefinedcallback is reserved for an error if it occurs. Then
undefinedundefinedcallback(err) is called. 2. The second argument (and the next ones if needed) are for
the successful result. Then undefinedundefinedcallback(null, result1, result2…) is
called.undefinedundefined
So the single undefinedundefinedcallback function is
used both for reporting errors and passing back results.undefinedundefined
From the first look, it's a viable way of asynchronous coding. And indeed it is. For one or maybe two nested calls it looks fine.
undefinedundefinedBut for multiple asynchronous actions that follow one after another we'll have code like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedloadScript(undefinedundefined'1.js'undefinedundefined,undefinedundefinedfunction(errorundefinedundefined, script) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedif (error) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedhandleError(error)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedelseundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefinedloadScript(undefinedundefined'2.js'undefinedundefined,undefinedundefinedfunction(errorundefinedundefined, script) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (error) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedhandleError(error)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedelseundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefinedloadScript(undefinedundefined'3.js'undefinedundefined,undefinedundefinedfunction(errorundefinedundefined, script) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (error) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedhandleError(error)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedelseundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefined// ...continue after all scripts are loaded (*)undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In the code above: 1. We load undefinedundefined1.js, then if there's no error.
2. We load undefinedundefined2.js, then if there's no error. 3. We load
undefinedundefined3.js, then if there's no error - do something else
undefinedundefined(*).undefinedundefined
As calls become more nested, the code
becomes deeper and increasingly more difficult to manage, especially if we have real code instead of
undefinedundefined... that may include more loops, conditional statements and so on.undefinedundefined
That's sometimes called "callback hell" or "pyramid of doom."
undefinedundefined undefinedundefinedundefinedundefinedundefinedundefined
The "pyramid" of nested calls grows to the right with every asynchronous action. Soon it spirals out of control.
undefinedundefinedSo this way of coding isn't very good.
undefinedundefinedWe can try to alleviate the problem by making every action a standalone function, like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedloadScript(undefinedundefined'1.js'undefinedundefined, step1)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedstep1(errorundefinedundefined, script) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (error) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedhandleError(error)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedelseundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefinedloadScript(undefinedundefined'2.js'undefinedundefined, step2)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedstep2(errorundefinedundefined, script) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (error) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedhandleError(error)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedelseundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefinedloadScript(undefinedundefined'3.js'undefinedundefined, step3)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedstep3(errorundefinedundefined, script) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (error) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedhandleError(error)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedelseundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...continue after all scripts are loaded (*)undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
See? It does the same, and there's no deep nesting now because we made every action a separate top-level function.
undefinedundefinedIt works, but the code looks like a torn apart spreadsheet. It's difficult to read, and you probably noticed that one needs to eye-jump between pieces while reading it. That's inconvenient, especially if the reader is not familiar with the code and doesn't know where to eye-jump.
undefinedundefinedAlso, the functions named undefinedundefinedstep* are all of single use, they are
created only to avoid the "pyramid of doom." No one is going to reuse them outside of the action chain. So there's a
bit of namespace cluttering here.undefinedundefined
We'd like to have something better.
undefinedundefinedLuckily, there are other ways to avoid such pyramids. One of the best ways is to use "promises," described in the next chapter.
undefinedundefinedImagine that you're a top singer, and fans ask day and night for your upcoming song.
undefinedundefinedTo get some relief, you promise to send it to them when it's published. You give your fans a list. They can fill in their email addresses, so that when the song becomes available, all subscribed parties instantly receive it. And even if something goes very wrong, say, a fire in the studio, so that you can't publish the song, they will still be notified.
undefinedundefinedEveryone is happy: you, because the people don't crowd you anymore, and fans, because they won't miss the song.
undefinedundefinedThis is a real-life analogy for things we often have in programming:
undefinedundefinedThe analogy isn't terribly accurate, because JavaScript promises are more complex than a simple subscription list: they have additional features and limitations. But it's fine to begin with.
undefinedundefinedThe constructor syntax for a promise object is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet promise undefinedundefined=undefinedundefinednewundefinedundefinedPromise(undefinedundefinedfunction(resolveundefinedundefined, reject) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// executor (the producing code, "singer")undefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The function passed to undefinedundefinednew Promise is called the
undefinedundefinedexecutor. When undefinedundefinednew Promise is created, the executor runs
automatically. It contains the producing code which should eventually produce the result. In terms of the analogy
above: the executor is the "singer".undefinedundefined
Its arguments
undefinedundefinedresolve and undefinedundefinedreject are callbacks provided by JavaScript
itself. Our code is only inside the executor.undefinedundefined
When the executor obtains the result, be it soon or late, doesn't matter, it should call one of these callbacks:
undefinedundefinedresolve(value) — if the job is finished successfully, with result
undefinedundefinedvalue.undefinedundefinedreject(error) — if an error has occurred, undefinedundefinederror is the
error object.undefinedundefinedSo to summarize: the executor runs
automatically and attempts to perform a job. When it is finished with the attempt, it calls
undefinedundefinedresolve if it was successful or undefinedundefinedreject if there was an
error.undefinedundefined
The undefinedundefinedpromise object returned by the
undefinedundefinednew Promise constructor has these internal properties:undefinedundefined
state — initially
undefinedundefined"pending", then changes to either undefinedundefined"fulfilled" when
undefinedundefinedresolve is called or undefinedundefined"rejected" when
undefinedundefinedreject is called.undefinedundefinedresult — initially undefinedundefinedundefined, then changes to
undefinedundefinedvalue when undefinedundefinedresolve(value) called or
undefinedundefinederror when undefinedundefinedreject(error) is called.undefinedundefined
So the executor eventually moves
undefinedundefinedpromise to one of these states:undefinedundefined
undefinedundefinedundefinedundefined
Later we'll see how "fans" can subscribe to these changes.
undefinedundefinedHere's an example of a promise constructor and a
simple executor function with "producing code" that takes time (via
undefinedundefinedsetTimeout):undefinedundefined
run let promise = new Promise(function(resolve, reject) { // the function is executed automatically when the promise is constructed
undefinedundefined// after 1 second signal that the job is done with the result "done" setTimeout(() => undefinedundefined!resolve("done")undefinedundefined/!, 1000); }); undefinedundefined
undefinedundefinedWe can see two things by running the code above:
undefinedundefinednew Promise).undefinedundefinedThe
executor receives two arguments: undefinedundefinedresolve and undefinedundefinedreject.
These functions are pre-defined by the JavaScript engine, so we don't need to create them. We should only call one
of them when ready.undefinedundefined
After one second of "processing" the executor calls
undefinedundefinedresolve("done") to produce the result. This changes the state of the
undefinedundefinedpromise object:undefinedundefined
undefinedundefinedundefinedundefined
That was an example of a successful job completion, a "fulfilled promise".
undefinedundefinedAnd now an example of the executor rejecting the promise with an error:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet promise undefinedundefined=undefinedundefinednewundefinedundefinedPromise(undefinedundefinedfunction(resolveundefinedundefined, reject) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// after 1 second signal that the job is finished with an errorundefinedundefinedundefinedundefinedundefinedundefinedsetTimeout(() undefinedundefined=>undefinedundefined*!*undefinedundefinedreject(undefinedundefinednewundefinedundefinedError(undefinedundefined"Whoops!"))undefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefined, 1000undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The call to undefinedundefinedreject(...) moves the promise object to
undefinedundefined"rejected" state:undefinedundefined
undefinedundefinedundefinedundefined
To summarize, the executor should perform a
job (usually something that takes time) and then call undefinedundefinedresolve or
undefinedundefinedreject to change the state of the corresponding promise object.undefinedundefined
A promise that is either resolved or rejected is called "settled", as opposed to an initially "pending" promise.
undefinedundefined
undefinedundefinedsmart header="There can be only a single result or an error" The executor should call only oneresolveundefinedundefinedor onereject`.
Any state change is final.undefinedundefined
All further calls of
undefinedundefinedresolve and undefinedundefinedreject are ignored:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet promise undefinedundefined=undefinedundefinednewundefinedundefinedPromise(undefinedundefinedfunction(resolveundefinedundefined, reject) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedresolve(undefinedundefined"done")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined rejectundefinedundefined(undefinedundefinednew Errorundefinedundefined(undefinedundefined"…"undefinedundefined))undefinedundefined; // ignoredundefinedundefinedundefinedundefinedundefinedundefined setTimeoutundefinedundefined(()undefinedundefined => resolveundefinedundefined(undefinedundefined"…"undefinedundefined))undefinedundefined; // ignoredundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The idea is that a job done by the executor may have only one result or an error.
undefinedundefinedAlso, undefinedundefinedresolve/undefinedundefinedreject expect only one
argument (or none) and will ignore additional arguments.
undefinedundefined
undefinedundefinedsmart header="Reject with `Error` objects" In case something goes wrong, the executor should call `reject`. That can be done with any type of argument (just like `resolve`). But it is recommended to use `Error` objects (or objects that inherit from `Error`). The reasoning for that will soon become apparent.undefinedundefined
undefinedundefinedsmart header="Immediately callingresolveundefinedundefined/rejectundefinedundefined" In practice, an executor usually does something asynchronously and callsresolveundefinedundefined/rejectundefinedundefinedafter some time, but it doesn't have to. We also can callresolveundefinedundefinedorreject`
immediately, like this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet promise undefinedundefined=undefinedundefinednewundefinedundefinedPromise(undefinedundefinedfunction(resolveundefinedundefined, reject) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// not taking our time to do the jobundefinedundefinedundefinedundefinedundefinedundefinedresolve(undefinedundefined123)undefinedundefined;undefinedundefined// immediately give the result: 123undefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
For instance, this might happen when we start to do a job but then see that everything has already been completed and cached.
undefinedundefinedThat's fine. We immediately have a resolved promise.
undefinedundefined
undefinedundefinedsmart header="The `state` and `result` are internal" The properties `state` and `result` of the Promise object are internal. We can't directly access them. We can use the methods `.then`/`.catch`/`.finally` for that. They are described below.undefinedundefined
A
Promise object serves as a link between the executor (the "producing code" or "singer") and the consuming functions
(the "fans"), which will receive the result or error. Consuming functions can be registered (subscribed) using methods
undefinedundefined.then, undefinedundefined.catch and
undefinedundefined.finally.undefinedundefined
The most important, fundamental one is undefinedundefined.then.undefinedundefined
The syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedpromise.undefinedundefinedthen(undefinedundefinedundefinedundefinedundefinedundefinedfunction(result) undefinedundefined{undefinedundefined*!*undefinedundefined/* handle a successful result */undefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefined },undefinedundefinedundefinedundefinedundefinedundefined functionundefinedundefined(undefinedundefinederrorundefinedundefined)undefinedundefined { undefinedundefined*undefinedundefined!undefinedundefined*undefinedundefined/undefinedundefined* handle an error undefinedundefined*undefinedundefined/*/!* }undefinedundefinedundefinedundefinedundefinedundefined);undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The first argument of undefinedundefined.then is a function that runs when the
promise is resolved, and receives the result.undefinedundefined
The second argument of
undefinedundefined.then is a function that runs when the promise is rejected, and receives the
error.undefinedundefined
For instance, here's a reaction to a successfully resolved promise:
undefinedundefinedrun let promise = new Promise(function(resolve, reject) { setTimeout(() => resolve("done!"), 1000); });
undefinedundefined// resolve runs the first function in .then promise.then( undefinedundefined! result => alert(result), // shows "done!" after 1 second undefinedundefined/! error => alert(error) // doesn't run ); undefinedundefined
undefinedundefinedThe first function was executed.
undefinedundefinedAnd in the case of a rejection, the second one:
undefinedundefinedrun let promise = new Promise(function(resolve, reject) { setTimeout(() => reject(new Error("Whoops!")), 1000); });
undefinedundefined// reject runs the second function in .then promise.then( result => alert(result), // doesn't run undefinedundefined! error => alert(error) // shows "Error: Whoops!" after 1 second undefinedundefined/! ); undefinedundefined
undefinedundefinedIf we're interested only in successful completions, then we can provide
only one function argument to undefinedundefined.then:undefinedundefined
run let promise = new Promise(resolve => { setTimeout(() => resolve("done!"), 1000); });
undefinedundefinedundefinedundefined! promise.then(alert); // shows "done!" after 1 second undefinedundefined/! undefinedundefined
undefinedundefinedIf we're interested only in
errors, then we can use undefinedundefinednull as the first argument:
undefinedundefined.then(null, errorHandlingFunction). Or we can use
undefinedundefined.catch(errorHandlingFunction), which is exactly the same:undefinedundefined
run let promise = new Promise((resolve, reject) => { setTimeout(() => reject(new Error("Whoops!")), 1000); });
undefinedundefinedundefinedundefined! // .catch(f) is the same as promise.then(null, f) promise.catch(alert); // shows "Error: Whoops!" after 1 second undefinedundefined/! undefinedundefined
undefinedundefinedThe call undefinedundefined.catch(f) is a complete analog of
undefinedundefined.then(null, f), it's just a shorthand.undefinedundefined
Just like there's a undefinedundefinedfinally clause in a
regular undefinedundefinedtry {...} catch {...}, there's undefinedundefinedfinally in
promises.undefinedundefined
The call undefinedundefined.finally(f) is similar to
undefinedundefined.then(f, f) in the sense that undefinedundefinedf always runs when the
promise is settled: be it resolve or reject.undefinedundefined
undefinedundefinedfinally is a good handler for performing cleanup, e.g. stopping our loading indicators,
as they are not needed anymore, no matter what the outcome is.undefinedundefined
Like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinednewundefinedundefinedPromise((resolveundefinedundefined, reject) undefinedundefined=>undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined/* do something that takes time, and then call resolve/reject */undefinedundefinedundefinedundefinedundefinedundefined})undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefined// runs when the promise is settled, doesn't matter successfully or notundefinedundefinedundefinedundefined .undefinedundefinedfinally(() undefinedundefined=> stop loading indicator)undefinedundefinedundefinedundefinedundefinedundefined// so the loading indicator is always stopped before we process the result/errorundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined .thenundefinedundefined(undefinedundefinedresult => show result, err => show errorundefinedundefined)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That said, undefinedundefinedfinally(f) isn't exactly an alias of
undefinedundefinedthen(f,f) though. There are few subtle differences:undefinedundefined
finally handler has no arguments.
In undefinedundefinedfinally we don't know whether the promise is successful or not. That's all right,
as our task is usually to perform "general" finalizing procedures.undefinedundefinedA undefinedundefinedfinally handler passes through results and errors to the next
handler.undefinedundefined
For instance, here the result is passed through
undefinedundefinedfinally to undefinedundefinedthen:
undefinedundefinedjs run new Promise((resolve, reject) => { setTimeout(() => resolve("result"), 2000) }) .finally(() => alert("Promise ready")) .then(result => alert(result)); // <-- .then handles the resultundefinedundefined
And here there's an error in the promise, passed through
undefinedundefinedfinally to undefinedundefinedcatch:undefinedundefined
undefinedundefinedjs run new Promise((resolve, reject) => { throw new Error("error"); }) .finally(() => alert("Promise ready")) .catch(err => alert(err)); // <-- .catch handles the error objectundefinedundefined
That's very convenient, because
undefinedundefinedfinally is not meant to process a promise result. So it passes it
through.undefinedundefined
We'll talk more about promise chaining and result-passing between handlers in the next chapter.
undefinedundefined
undefinedundefinedsmart header="We can attach handlers to settled promises" If a promise is pending,.then/catch/finally`
handlers wait for it. Otherwise, if a promise has already settled, they just run:undefinedundefined
run // the promise becomes resolved immediately upon creation let promise = new Promise(resolve => resolve("done!"));
undefinedundefinedpromise.then(alert); // done! (shows up right now)
undefinedundefinedundefinedundefined
Note that this makes promises more powerful than the real life "subscription list" scenario. If the singer has already released their song and then a person signs up on the subscription list, they probably won't receive that song. Subscriptions in real life must be done prior to the event.
Promises are more flexible. We can add handlers any time: if the result is already there, they just execute.undefinedundefined
undefinedundefinedNext, let's see more practical examples of how promises can help us write asynchronous code.
undefinedundefined
We've got the undefinedundefinedloadScript function for loading a script from the previous
chapter.undefinedundefined
Here's the callback-based variant, just to remind us of it:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedloadScript(srcundefinedundefined, callback) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet script undefinedundefined=undefinedundefineddocument.undefinedundefinedcreateElement(undefinedundefined'script')undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedscript.undefinedundefinedsrcundefinedundefined= srcundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedscript.undefinedundefinedonloadundefinedundefined= () undefinedundefined=>undefinedundefinedcallback(undefinedundefinednullundefinedundefined, script)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedscript.undefinedundefinedonerrorundefinedundefined= () undefinedundefined=>undefinedundefinedcallback(undefinedundefinednewundefinedundefinedError(undefinedundefined`Script load error for undefinedundefined${srcundefinedundefined}undefinedundefined`))undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefineddocument.undefinedundefinedhead.undefinedundefinedappend(script)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Let's rewrite it using Promises.
undefinedundefinedThe new function
undefinedundefinedloadScript will not require a callback. Instead, it will create and return a Promise
object that resolves when the loading is complete. The outer code can add handlers (subscribing functions) to it using
undefinedundefined.then:undefinedundefined
run function loadScript(src) { return new Promise(function(resolve, reject) { let script = document.createElement(‘script'); script.src = src;
undefinedundefinedundefinedundefinedscript.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Script load error for ${src}`));
document.head.append(script);undefinedundefinedundefinedundefined}); }
undefinedundefinedUsage:
undefinedundefinedrun let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");
undefinedundefined
promise.then( script => alert(undefinedundefined${script.src} is loaded!), error =>
alert(undefinedundefinedError: ${error.message}) );undefinedundefined
promise.then(script => alert(‘Another handler…'));
undefinedundefinedWe can immediately see a few benefits over the callback-based pattern:
undefinedundefinedSo promises give us better code flow and flexibility. But there's more. We'll see that in the next chapters.
undefinedundefinedLet's return to the problem mentioned in the chapter undefinedundefinedinfo:callbacks: we have a sequence of asynchronous tasks to be performed one after another — for instance, loading scripts. How can we code it well?undefinedundefined
undefinedundefinedPromises provide a couple of recipes to do that.
undefinedundefinedIn this chapter we cover promise chaining.
undefinedundefinedIt looks like this:
undefinedundefinedrun new Promise(function(resolve, reject) {
undefinedundefinedsetTimeout(() => resolve(1), 1000); // (*)
undefinedundefined}).then(function(result) { // (**)
undefinedundefinedalert(result); // 1 return result * 2;
undefinedundefined}).then(function(result) { // (***)
undefinedundefinedalert(result); // 2 return result * 2;
undefinedundefined}).then(function(result) {
undefinedundefinedalert(result); // 4 return result * 2;
undefinedundefined});
undefinedundefinedThe idea is that the result is passed through the chain of undefinedundefined.then
handlers.undefinedundefined
Here the flow is: 1. The initial promise resolves in 1 second
undefinedundefined(*), 2. Then the undefinedundefined.then handler is called
undefinedundefined(**). 3. The value that it returns is passed to the next
undefinedundefined.then handler undefinedundefined(***) 4. …and so on.undefinedundefined
As the result is passed along the chain of handlers, we can see a sequence of
undefinedundefinedalert calls: undefinedundefined1 -> undefinedundefined2
-> undefinedundefined4.undefinedundefined
undefinedundefinedundefinedundefined
The whole thing works, because a call to
undefinedundefinedpromise.then returns a promise, so that we can call the next
undefinedundefined.then on it.undefinedundefined
When a handler returns a value,
it becomes the result of that promise, so the next undefinedundefined.then is called with
it.undefinedundefined
undefinedundefinedA classic newbie error: technically we can
also add many undefinedundefined.then to a single promise. This is not
chaining.undefinedundefinedundefinedundefined
For example: run let promise = new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000); });
undefinedundefinedpromise.then(function(result) { alert(result); // 1 return result * 2; });
undefinedundefinedpromise.then(function(result) { alert(result); // 1 return result * 2; });
undefinedundefinedpromise.then(function(result) { alert(result); // 1 return result * 2; });
undefinedundefinedWhat we did here is just several handlers to one promise. They don't pass the result to each other; instead they process it independently.
undefinedundefinedHere's the picture (compare it with the chaining above):
undefinedundefinedundefinedundefinedundefinedundefined
All undefinedundefined.then on the same promise get the same result - the result of
that promise. So in the code above all undefinedundefinedalert show the same:
undefinedundefined1.undefinedundefined
In practice we rarely need multiple handlers for one promise. Chaining is used much more often.
undefinedundefinedA handler, used in undefinedundefined.then(handler) may create and
return a promise.undefinedundefined
In that case further handlers wait until it settles, and then get its result.
undefinedundefinedFor instance:
undefinedundefinedrun new Promise(function(resolve, reject) {
undefinedundefinedsetTimeout(() => resolve(1), 1000);
undefinedundefined}).then(function(result) {
undefinedundefinedalert(result); // 1
undefinedundefinedundefinedundefined! return new Promise((resolve, reject) => { // (undefinedundefined) setTimeout(() => resolve(result 2), 1000); }); undefinedundefined/!undefinedundefined
undefinedundefined}).then(function(result) { // (**)
undefinedundefinedalert(result); // 2
undefinedundefinedreturn new Promise((resolve, reject) => { setTimeout(() => resolve(result * 2), 1000); });
undefinedundefined}).then(function(result) {
undefinedundefinedalert(result); // 4
undefinedundefined});
undefinedundefinedHere the first undefinedundefined.then shows undefinedundefined1 and
returns undefinedundefinednew Promise(…) in the line undefinedundefined(*). After one second
it resolves, and the result (the argument of undefinedundefinedresolve, here it's
undefinedundefinedresult * 2) is passed on to handler of the second undefinedundefined.then.
That handler is in the line undefinedundefined(**), it shows undefinedundefined2 and does
the same thing.undefinedundefined
So the output is the same as in the previous example: 1
-> 2 -> 4, but now with 1 second delay between undefinedundefinedalert calls.undefinedundefined
Returning promises allows us to build chains of asynchronous actions.
undefinedundefinedLet's use this feature with the promisified
undefinedundefinedloadScript, defined in the undefinedundefinedprevious chapter, to load scripts one by one, in
sequence:undefinedundefined
undefinedundefinedjs run loadScript("/article/promise-chaining/one.js") .then(function(script) { return loadScript("/article/promise-chaining/two.js"); }) .then(function(script) { return loadScript("/article/promise-chaining/three.js"); }) .then(function(script) { // use functions declared in scripts // to show that they indeed loaded one(); two(); three(); });undefinedundefined
This code can be made bit shorter with arrow functions:
undefinedundefined
undefinedundefinedjs run loadScript("/article/promise-chaining/one.js") .then(script => loadScript("/article/promise-chaining/two.js")) .then(script => loadScript("/article/promise-chaining/three.js")) .then(script => { // scripts are loaded, we can use functions declared there one(); two(); three(); });undefinedundefined
Here each undefinedundefinedloadScript call returns a promise, and the next
undefinedundefined.then runs when it resolves. Then it initiates the loading of the next script. So
scripts are loaded one after another.undefinedundefined
We can add more asynchronous actions to the chain. Please note that the code is still "flat" — it grows down, not to the right. There are no signs of the "pyramid of doom".
undefinedundefinedTechnically, we could add undefinedundefined.then directly to
each undefinedundefinedloadScript, like this:undefinedundefined
undefinedundefinedjs run loadScript("/article/promise-chaining/one.js").then(script1 => { loadScript("/article/promise-chaining/two.js").then(script2 => { loadScript("/article/promise-chaining/three.js").then(script3 => { // this function has access to variables script1, script2 and script3 one(); two(); three(); }); }); });undefinedundefined
This code does the same: loads 3 scripts in sequence. But it "grows to the right". So we have the same problem as with callbacks.
undefinedundefinedPeople who start to use promises sometimes don't know about chaining, so they write it this way. Generally, chaining is preferred.
undefinedundefinedSometimes it's ok
to write undefinedundefined.then directly, because the nested function has access to the outer scope. In
the example above the most nested callback has access to all variables undefinedundefinedscript1,
undefinedundefinedscript2, undefinedundefinedscript3. But that's an exception rather than a
rule.undefinedundefined
undefinedundefinedsmart header="Thenables" To be precise, a handler may return not exactly a promise, but a so-called "thenable" object - an arbitrary object that has a method.then`.
It will be treated the same way as a promise.undefinedundefined
The idea is that 3rd-party
libraries may implement "promise-compatible" objects of their own. They can have an extended set of methods, but also
be compatible with native promises, because they implement undefinedundefined.then.undefinedundefined
Here's an example of a thenable object:
undefinedundefinedrun class Thenable { constructor(num) { this.num = num; } then(resolve, reject) { alert(resolve); // function() { native code } // resolve with this.numundefinedundefined2 after the 1 second setTimeout(() => resolve(this.num 2), 1000); // (**) } }undefinedundefined
undefinedundefinednew Promise(resolve => resolve(1)) .then(result => { undefinedundefined! return new Thenable(result); // (undefinedundefined) /!* }) .then(alert); // shows 2 after 1000msundefinedundefined
undefinedundefinedundefinedundefined
JavaScript checks the object returned by the `.then` handler in line `(*)`: if it has a callable method named `then`, then it calls that method providing native functions `resolve`, `reject` as arguments (similar to an executor) and waits until one of them is called. In the example above `resolve(2)` is called after 1 second `(**)`. Then the result is passed further down the chain.
This feature allows us to integrate custom objects with promise chains without having to inherit from `Promise`.undefinedundefined
undefinedundefinedIn frontend programming promises are often used for network requests. So let's see an extended example of that.
undefinedundefinedWe'll use the undefinedundefinedfetch method to load the information about the user from the remote server. It has a lot of optional parameters covered in undefinedundefinedseparate chapters, but the basic syntax is quite simple:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet promise undefinedundefined=undefinedundefinedfetch(url)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
This makes a network request to the undefinedundefinedurl and returns a promise.
The promise resolves with a undefinedundefinedresponse object when the remote server responds with
headers, but undefinedundefinedbefore the full response is downloaded.undefinedundefined
To read the full response, we should call the method
undefinedundefinedresponse.text(): it returns a promise that resolves when the full text is downloaded
from the remote server, with that text as a result.undefinedundefined
The code below makes a
request to undefinedundefineduser.json and loads its text from the server:undefinedundefined
undefinedundefinedjs run fetch('/article/promise-chaining/user.json') // .then below runs when the remote server responds .then(function(response) { // response.text() returns a new promise that resolves with the full response text // when it loads return response.text(); }) .then(function(text) { // ...and here's the content of the remote file alert(text); // {"name": "iliakan", "isAdmin": true} });undefinedundefined
The undefinedundefinedresponse object returned from
undefinedundefinedfetch also includes the method undefinedundefinedresponse.json() that
reads the remote data and parses it as JSON. In our case that's even more convenient, so let's switch to
it.undefinedundefined
We'll also use arrow functions for brevity:
undefinedundefined
undefinedundefinedjs run // same as above, but response.json() parses the remote content as JSON fetch('/article/promise-chaining/user.json') .then(response => response.json()) .then(user => alert(user.name)); // iliakan, got user nameundefinedundefined
Now let's do something with the loaded user.
undefinedundefinedFor instance, we can make one more request to GitHub, load the user profile and show the avatar:
undefinedundefined
``undefinedundefinedjs run // Make a request for user.json fetch('/article/promise-chaining/user.json') // Load it as json .then(response => response.json()) // Make a request to GitHub .then(user => fetch(https://api.github.com/users/${user.name}`))
// Load the response as json .then(response => response.json()) // Show the avatar image (githubUser.avatar_url)
for 3 seconds (maybe animate it) .then(githubUser => { let img = document.createElement(‘img'); img.src =
githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img);undefinedundefined
undefinedundefinedsetTimeout(() => img.remove(), 3000); // (*)undefinedundefined
undefinedundefined});
undefinedundefinedThe code works; see comments about the details. However, there's a potential problem in it, a typical error for those who begin to use promises.
undefinedundefinedLook at the line
undefinedundefined(*): how can we do something undefinedundefinedafter the avatar has finished
showing and gets removed? For instance, we'd like to show a form for editing that user or something else. As of now,
there's no way.undefinedundefined
To make the chain extendable, we need to return a promise that resolves when the avatar finishes showing.
undefinedundefinedLike this:
undefinedundefined
``undefinedundefinedjs run fetch('/article/promise-chaining/user.json') .then(response => response.json()) .then(user => fetch(https://api.github.com/users/${user.name}`))
.then(response => response.json()) undefinedundefined! .then(githubUser => new
Promise(function(resolve, reject) { // (undefinedundefined) /!* let img = document.createElement(‘img');
img.src = githubUser.avatar_url; img.className = "promise-avatar-example";
document.body.append(img);undefinedundefined
undefinedundefinedsetTimeout(() => {
img.remove();undefinedundefinedundefinedundefinedundefinedundefined! resolve(githubUser); //
(**) undefinedundefined/! }, 3000); })) // triggers after 3 seconds .then(githubUser =>
alert(undefinedundefinedFinished showing ${githubUser.name}));
undefinedundefined
That is, the undefinedundefined.then handler in line
undefinedundefined(*) now returns undefinedundefinednew Promise, that becomes settled only
after the call of undefinedundefinedresolve(githubUser) in
undefinedundefinedsetTimeoutundefinedundefined(**). The next
undefinedundefined.then in the chain will wait for that.undefinedundefined
As a good practice, an asynchronous action should always return a promise. That makes it possible to plan actions after it; even if we don't plan to extend the chain now, we may need it later.
undefinedundefinedFinally, we can split the code into reusable functions:
undefinedundefinedrun function loadJson(url) { return fetch(url) .then(response => response.json()); }
undefinedundefinedfunction loadGithubUser(name) { return
fetch(undefinedundefinedhttps://api.github.com/users/${name}) .then(response => response.json());
}undefinedundefined
function showAvatar(githubUser) { return new Promise(function(resolve, reject) { let img = document.createElement(‘img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img);
undefinedundefinedundefinedundefinedsetTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);undefinedundefinedundefinedundefined}); }
undefinedundefined// Use them:
loadJson(‘/article/promise-chaining/user.json') .then(user => loadGithubUser(user.name)) .then(showAvatar)
.then(githubUser => alert(undefinedundefinedFinished showing ${githubUser.name})); // …
undefinedundefined
If a
undefinedundefined.then (or undefinedundefinedcatch/finally, doesn't matter) handler returns
a promise, the rest of the chain waits until it settles. When it does, its result (or error) is passed
further.undefinedundefined
Here's a full picture:
undefinedundefined
undefinedundefinedundefinedundefined
Promise chains are great at error handling. When a promise rejects, the control jumps to the closest rejection handler. That's very convenient in practice.
undefinedundefinedFor instance, in the code below the URL to undefinedundefinedfetch is
wrong (no such site) and undefinedundefined.catch handles the error:undefinedundefined
undefinedundefinedjs run *!* fetch('https://no-such-server.blabla') // rejects */!* .then(response => response.json()) .catch(err => alert(err)) // TypeError: failed to fetch (the text may vary)undefinedundefined
As you can see, the undefinedundefined.catch doesn't have to be immediate. It may
appear after one or maybe several undefinedundefined.then.undefinedundefined
Or,
maybe, everything is all right with the site, but the response is not valid JSON. The easiest way to catch all errors
is to append undefinedundefined.catch to the end of chain:undefinedundefined
``undefinedundefinedjs run fetch('/article/promise-chaining/user.json') .then(response => response.json()) .then(user => fetch(https://api.github.com/users/${user.name}`))
.then(response => response.json()) .then(githubUser => new Promise((resolve, reject) => { let img =
document.createElement(‘img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example";
document.body.append(img);undefinedundefined
undefinedundefinedsetTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);undefinedundefinedundefinedundefined})) undefinedundefined! .catch(error => alert(error.message)); undefinedundefined/! undefinedundefined
undefinedundefinedNormally, such undefinedundefined.catch doesn't trigger at
all. But if any of the promises above rejects (a network problem or invalid json or whatever), then it would catch
it.undefinedundefined
The code of a promise executor and promise handlers has an "invisible undefinedundefinedtry..catch"
around it. If an exception happens, it gets caught and treated as a rejection.undefinedundefined
For instance, this code:
undefinedundefined
undefinedundefinedjs run new Promise((resolve, reject) => { *!* throw new Error("Whoops!"); */!* }).catch(alert); // Error: Whoops!undefinedundefined
…Works exactly the same as this:
undefinedundefined
undefinedundefinedjs run new Promise((resolve, reject) => { *!* reject(new Error("Whoops!")); */!* }).catch(alert); // Error: Whoops!undefinedundefined
The "invisible undefinedundefinedtry..catch" around the executor automatically
catches the error and turns it into rejected promise.undefinedundefined
This happens not only
in the executor function, but in its handlers as well. If we undefinedundefinedthrow inside a
undefinedundefined.then handler, that means a rejected promise, so the control jumps to the nearest error
handler.undefinedundefined
Here's an example:
undefinedundefined
undefinedundefinedjs run new Promise((resolve, reject) => { resolve("ok"); }).then((result) => { *!* throw new Error("Whoops!"); // rejects the promise */!* }).catch(alert); // Error: Whoops!undefinedundefined
This happens for all errors, not just those caused by the undefinedundefinedthrow
statement. For example, a programming error:undefinedundefined
undefinedundefinedjs run new Promise((resolve, reject) => { resolve("ok"); }).then((result) => { *!* blabla(); // no such function */!* }).catch(alert); // ReferenceError: blabla is not definedundefinedundefined
The final undefinedundefined.catch not only catches explicit rejections, but also
accidental errors in the handlers above.undefinedundefined
As we already noticed, undefinedundefined.catch at the end of the chain is similar to
undefinedundefinedtry..catch. We may have as many undefinedundefined.then handlers as we
want, and then use a single undefinedundefined.catch at the end to handle errors in all of
them.undefinedundefined
In a regular undefinedundefinedtry..catch we can analyze
the error and maybe rethrow it if it can't be handled. The same thing is possible for promises.undefinedundefined
If we undefinedundefinedthrow inside undefinedundefined.catch, then the
control goes to the next closest error handler. And if we handle the error and finish normally, then it continues to
the next closest successful undefinedundefined.then handler.undefinedundefined
In
the example below the undefinedundefined.catch successfully handles the error:undefinedundefined
run // the execution: catch -> then new Promise((resolve, reject) => {
undefinedundefinedthrow new Error("Whoops!");
undefinedundefined}).catch(function(error) {
undefinedundefinedalert("The error is handled, continue normally");
undefinedundefined}).then(() => alert("Next successful handler runs"));
undefinedundefinedHere the undefinedundefined.catch block finishes normally. So the next successful
undefinedundefined.then handler is called.undefinedundefined
In the example below
we see the other situation with undefinedundefined.catch. The handler undefinedundefined(*)
catches the error and just can't handle it (e.g. it only knows how to handle undefinedundefinedURIError),
so it throws it again:undefinedundefined
run // the execution: catch -> catch new Promise((resolve, reject) => {
undefinedundefinedthrow new Error("Whoops!");
undefinedundefined}).catch(function(error) { // (*)
undefinedundefinedif (error instanceof URIError) { // handle it } else { alert("Can't handle such error");
undefinedundefinedundefinedundefined! throw error; // throwing this or another error jumps to the next catch undefinedundefined/! }undefinedundefined
undefinedundefined}).then(function() { /* doesn't run here */ }).catch(error => { // (**)
undefinedundefined
alert(undefinedundefinedThe unknown error has occurred: ${error}); // don't return anything =>
execution goes the normal wayundefinedundefined
});
undefinedundefinedThe execution jumps from the first
undefinedundefined.catchundefinedundefined(*) to the next one
undefinedundefined(**) down the chain.undefinedundefined
What happens when an error is not handled? For
instance, we forgot to append undefinedundefined.catch to the end of the chain, like
here:undefinedundefined
undefinedundefinedjs untrusted run refresh new Promise(function() { noSuchFunction(); // Error here (no such function) }) .then(() => { // successful promise handlers, one or more }); // without .catch at the end!undefinedundefined
In case of an error, the promise becomes rejected, and the execution should jump to the closest rejection handler. But there is none. So the error gets "stuck". There's no code to handle it.
undefinedundefinedIn practice, just like with regular unhandled errors in code, it means that something has gone terribly wrong.
undefinedundefinedWhat happens when a regular error occurs and is not caught by
undefinedundefinedtry..catch? The script dies with a message in the console. A similar thing happens with
unhandled promise rejections.undefinedundefined
The JavaScript engine tracks such rejections and generates a global error in that case. You can see it in the console if you run the example above.
undefinedundefinedIn the browser we can catch such errors using the event
undefinedundefinedunhandledrejection:undefinedundefined
run undefinedundefined! window.addEventListener(‘unhandledrejection', function(event) { // the event object has two special properties: alert(event.promise); // [object Promise] - the promise that generated the error alert(event.reason); // Error: Whoops! - the unhandled error object }); undefinedundefined/!undefinedundefined
undefinedundefinednew Promise(function() { throw new Error("Whoops!"); }); // no catch to handle the error
undefinedundefinedThe event is the part of the undefinedundefinedHTML standard.undefinedundefined
undefinedundefinedIf an error occurs, and there's no
undefinedundefined.catch, the undefinedundefinedunhandledrejection handler triggers, and
gets the undefinedundefinedevent object with the information about the error, so we can do
something.undefinedundefined
Usually such errors are unrecoverable, so our best way out is to inform the user about the problem and probably report the incident to the server.
undefinedundefinedIn non-browser environments like Node.js there are other ways to track unhandled errors.
undefinedundefined.catch handles
errors in promises of all kinds: be it a undefinedundefinedreject() call, or an error thrown in a
handler.undefinedundefined.catch exactly
in places where we want to handle errors and know how to handle them. The handler should analyze errors (custom
error classes help) and rethrow unknown ones (maybe they are programming mistakes).undefinedundefined.catch at all, if there's no way to recover
from an error.undefinedundefinedunhandledrejection event handler (for browsers, and analogs for other environments)
to track unhandled errors and inform the user (and probably our server) about them, so that our app never "just
dies".undefinedundefinedThere are 6 static methods in the undefinedundefinedPromise class. We'll quickly cover
their use cases here.undefinedundefined
Let's say we want many promises to execute in parallel and wait until all of them are ready.
undefinedundefinedFor instance, download several URLs in parallel and process the content once they are all done.
undefinedundefinedThat's what undefinedundefinedPromise.all is for.undefinedundefined
The syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet promise undefinedundefined=undefinedundefinedPromise.undefinedundefinedall([...undefinedundefinedpromises...])undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedPromise.all takes an array of promises (it technically can be
any iterable, but is usually an array) and returns a new promise.undefinedundefined
The new promise resolves when all listed promises are settled, and the array of their results becomes its result.
undefinedundefinedFor instance, the undefinedundefinedPromise.all below settles after 3 seconds, and
then its result is an array undefinedundefined[1, 2, 3]:undefinedundefined
undefinedundefinedjs run Promise.all([ new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1 new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2 new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3 ]).then(alert); // 1,2,3 when promises are ready: each promise contributes an array memberundefinedundefined
Please note that the order of the resulting array members is the same as in its source promises. Even though the first promise takes the longest time to resolve, it's still first in the array of results.
undefinedundefinedA common trick is to map an array of job data into an array of promises, and then wrap that
into undefinedundefinedPromise.all.undefinedundefined
For instance, if we have an array of URLs, we can fetch them all like this:
undefinedundefinedrun let urls = [ ‘https://api.github.com/users/iliakan', ‘https://api.github.com/users/remy', ‘https://api.github.com/users/jeresig'];
undefinedundefined// map every url to the promise of the fetch let requests = urls.map(url => fetch(url));
undefinedundefined// Promise.all waits until all jobs are resolved Promise.all(requests) .then(responses =>
responses.forEach( response => alert(undefinedundefined${response.url}: ${response.status}) ));
undefinedundefined
A bigger example with fetching user information for an array of GitHub users by their names (we could fetch an array of goods by their ids, the logic is identical):
undefinedundefinedrun let names = [‘iliakan', ‘remy', ‘jeresig'];
undefinedundefinedlet requests = names.map(name =>
fetch(undefinedundefinedhttps://api.github.com/users/${name}));undefinedundefined
Promise.all(requests) .then(responses => { // all responses are resolved successfully for(let response of
responses) { alert(undefinedundefined${response.url}: ${response.status}); // shows 200 for every url
}undefinedundefined
undefinedundefinedreturn responses;undefinedundefinedundefinedundefined}) // map array of responses into an array of response.json() to read their content .then(responses => Promise.all(responses.map(r => r.json()))) // all JSON answers are parsed: "users" is the array of them .then(users => users.forEach(user => alert(user.name)));
undefinedundefinedundefinedundefinedIf any of the promises is rejected, the promise returned by
undefinedundefinedPromise.all immediately rejects with that
error.undefinedundefinedundefinedundefined
For instance:
undefinedundefined
undefinedundefinedjs run Promise.all([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), *!* new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)), */!* new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).catch(alert); // Error: Whoops!undefinedundefined
Here the second promise rejects in two seconds. That leads to an immediate rejection of
undefinedundefinedPromise.all, so undefinedundefined.catch executes: the rejection error
becomes the outcome of the entire undefinedundefinedPromise.all.undefinedundefined
``undefinedundefinedwarn header="In case of an error, other promises are ignored" If one promise rejects,Promise.all`
immediately rejects, completely forgetting about the other ones in the list. Their results are
ignored.undefinedundefined
For example, if there are multiple
undefinedundefinedfetch calls, like in the example above, and one fails, the others will still continue
to execute, but undefinedundefinedPromise.all won't watch them anymore. They will probably settle, but
their results will be ignored.undefinedundefined
undefinedundefinedPromise.all
does nothing to cancel them, as there's no concept of "cancellation" in promises. In undefinedundefinedanother chapter we'll cover undefinedundefinedAbortController that can help
with that, but it's not a part of the Promise API.
undefinedundefined
undefinedundefinedsmart header="Promise.all(iterable)undefinedundefinedallows non-promise \"regular\" values initerableundefinedundefined" Normally,Promise.all(…)`
accepts an iterable (in most cases an array) of promises. But if any of those objects is not a promise, it's passed to
the resulting array "as is".undefinedundefined
For instance, here the results are
undefinedundefined[1, 2, 3]:undefinedundefined
undefinedundefinedjs run Promise.all([ new Promise((resolve, reject) => { setTimeout(() => resolve(1), 1000) }), 2, 3 ]).then(alert); // 1, 2, 3undefinedundefined
So we are able to pass ready values to undefinedundefinedPromise.all where
convenient.
undefinedundefined
[recent browser="new"]
undefinedundefinedundefinedundefinedPromise.all rejects as a whole if any
promise rejects. That's good for "all or nothing" cases, when we need undefinedundefinedall results
successful to proceed:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedPromise.undefinedundefinedall([undefinedundefinedundefinedundefinedundefinedundefinedfetch(undefinedundefined'/template.html')undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedfetch(undefinedundefined'/style.css')undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedfetch(undefinedundefined'/data.json')undefinedundefinedundefinedundefined]).undefinedundefinedthen(render)undefinedundefined;undefinedundefined// render method needs results of all fetchesundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedPromise.allSettled just waits for all promises to settle,
regardless of the result. The resulting array has:undefinedundefined
{status:"fulfilled", value:result} for successful responses,undefinedundefined{status:"rejected", reason:error} for errors.undefinedundefined
For example, we'd like to fetch the information about multiple users. Even if one request fails, we're still interested in the others.
undefinedundefinedLet's use
undefinedundefinedPromise.allSettled:undefinedundefined
run let urls = [ ‘https://api.github.com/users/iliakan', ‘https://api.github.com/users/remy', ‘https://no-such-url'];
undefinedundefinedPromise.allSettled(urls.map(url => fetch(url))) .then(results => { // (*)
results.forEach((result, num) => { if (result.status == "fulfilled") {
alert(undefinedundefined${urls[num]}: ${result.value.status}); } if (result.status == "rejected") {
alert(undefinedundefined${urls[num]}: ${result.reason}); } }); });
undefinedundefined
The undefinedundefinedresults in the line
undefinedundefined(*) above will be:undefinedundefined
undefinedundefinedundefinedundefined[undefinedundefinedundefinedundefined{undefinedundefinedstatusundefinedundefined:undefinedundefined'fulfilled'undefinedundefined,undefinedundefinedvalueundefinedundefined: ...undefinedundefinedresponse...undefinedundefined},undefinedundefinedundefinedundefinedundefinedundefined{undefinedundefinedstatusundefinedundefined:undefinedundefined'fulfilled'undefinedundefined,undefinedundefinedvalueundefinedundefined: ...undefinedundefinedresponse...undefinedundefined},undefinedundefinedundefinedundefinedundefinedundefined{undefinedundefinedstatusundefinedundefined:undefinedundefined'rejected'undefinedundefined,undefinedundefinedreasonundefinedundefined: ...undefinedundefinederrorundefinedundefinedobject...undefinedundefined}undefinedundefinedundefinedundefined]undefinedundefinedundefinedundefined
undefinedundefined
So for each promise we get its status and
undefinedundefinedvalue/error.undefinedundefined
If the browser doesn't support undefinedundefinedPromise.allSettled, it's easy to
polyfill:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedif (undefinedundefined!undefinedundefinedPromise.undefinedundefinedallSettled) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedconst rejectHandler undefinedundefined= reason undefinedundefined=> (undefinedundefined{undefinedundefinedstatusundefinedundefined:undefinedundefined'rejected'undefinedundefined, reason undefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedconst resolveHandler undefinedundefined= value undefinedundefined=> (undefinedundefined{undefinedundefinedstatusundefinedundefined:undefinedundefined'fulfilled'undefinedundefined, value undefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedPromise.undefinedundefinedallSettledundefinedundefined=undefinedundefinedfunction (promises) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedconst convertedPromises undefinedundefined=undefinedundefinedpromises.undefinedundefinedmap(p undefinedundefined=>undefinedundefinedPromise.undefinedundefinedresolve(p).undefinedundefinedthen(resolveHandlerundefinedundefined, rejectHandler))undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedPromise.undefinedundefinedall(convertedPromises)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In this code, undefinedundefinedpromises.map takes input values, turns them into
promises (just in case a non-promise was passed) with undefinedundefinedp => Promise.resolve(p), and
then adds undefinedundefined.then handler to every one.undefinedundefined
That
handler turns a successful result undefinedundefinedvalue into
undefinedundefined{status:'fulfilled', value}, and an error undefinedundefinedreason into
undefinedundefined{status:'rejected', reason}. That's exactly the format of
undefinedundefinedPromise.allSettled.undefinedundefined
Now we can use
undefinedundefinedPromise.allSettled to get the results of undefinedundefinedall given promises,
even if some of them reject.undefinedundefined
Similar to undefinedundefinedPromise.all, but waits only for the first settled promise
and gets its result (or error).undefinedundefined
The syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet promise undefinedundefined=undefinedundefinedPromise.undefinedundefinedrace(iterable)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
For instance, here the result will be undefinedundefined1:undefinedundefined
undefinedundefinedjs run Promise.race([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).then(alert); // 1undefinedundefined
The first promise here was fastest, so it became the result. After the first settled promise "wins the race", all further results/errors are ignored.
undefinedundefinedSimilar to undefinedundefinedPromise.race, but waits only for the first fulfilled
promise and gets its result. If all of the given promises are rejected, then the returned promise is rejected with
undefinedundefinedundefinedundefinedAggregateErrorundefinedundefined
- a special error object that stores all promise errors in its undefinedundefinederrors
property.undefinedundefined
The syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet promise undefinedundefined=undefinedundefinedPromise.undefinedundefinedany(iterable)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
For instance, here the result will be undefinedundefined1:undefinedundefined
undefinedundefinedjs run Promise.any([ new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)), new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).then(alert); // 1undefinedundefined
The first promise here was fastest, but it was rejected, so the second promise became the result. After the first fulfilled promise "wins the race", all further results are ignored.
undefinedundefinedHere's an example when all promises fail:
undefinedundefined
undefinedundefinedjs run Promise.any([ new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ouch!")), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("Error!")), 2000)) ]).catch(error => { console.log(error.constructor.name); // AggregateError console.log(error.errors[0]); // Error: Ouch! console.log(error.errors[1]); // Error: Error });undefinedundefined
As you can see, error objects for failed promises are available in the
undefinedundefinederrors property of the undefinedundefinedAggregateError
object.undefinedundefined
Methods undefinedundefinedPromise.resolve and
undefinedundefinedPromise.reject are rarely needed in modern code, because
undefinedundefinedasync/await syntax (we'll cover it undefinedundefineda bit
later) makes them somewhat obsolete.undefinedundefined
We cover them here for
completeness and for those who can't use undefinedundefinedasync/await for some reason.undefinedundefined
undefinedundefinedPromise.resolve(value) creates a resolved promise with the result
undefinedundefinedvalue.undefinedundefined
Same as:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet promise undefinedundefined=undefinedundefinednewundefinedundefinedPromise(resolve undefinedundefined=>undefinedundefinedresolve(value))undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The method is used for compatibility, when a function is expected to return a promise.
undefinedundefinedFor example, the undefinedundefinedloadCached function below fetches a URL and
remembers (caches) its content. For future calls with the same URL it immediately gets the previous content from
cache, but uses undefinedundefinedPromise.resolve to make a promise of it, so the returned value is
always a promise:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet cache undefinedundefined=undefinedundefinednewundefinedundefinedMap()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedloadCached(url) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (undefinedundefinedcache.undefinedundefinedhas(url)) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedPromise.undefinedundefinedresolve(undefinedundefinedcache.undefinedundefinedget(url))undefinedundefined;undefinedundefined// (*)undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined return fetchundefinedundefined(undefinedundefinedurlundefinedundefined)undefinedundefinedundefinedundefinedundefinedundefined .thenundefinedundefined(undefinedundefinedresponse => response.textundefinedundefined())undefinedundefinedundefinedundefinedundefinedundefined .thenundefinedundefined(undefinedundefinedtext => {undefinedundefinedundefinedundefinedundefinedundefined cache.setundefinedundefined(undefinedundefinedurl,textundefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined return text;undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
We can write undefinedundefinedloadCached(url).then(…), because the function is
guaranteed to return a promise. We can always use undefinedundefined.then after
undefinedundefinedloadCached. That's the purpose of undefinedundefinedPromise.resolve in the
line undefinedundefined(*).undefinedundefined
undefinedundefinedPromise.reject(error) creates a rejected promise with
undefinedundefinederror.undefinedundefined
Same as:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet promise undefinedundefined=undefinedundefinednewundefinedundefinedPromise((resolveundefinedundefined, reject) undefinedundefined=>undefinedundefinedreject(error))undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In practice, this method is almost never used.
undefinedundefinedThere are 6 static methods of undefinedundefinedPromise class:undefinedundefined
Promise.all(promises) - waits
for all promises to resolve and returns an array of their results. If any of the given promises rejects, it becomes
the error of undefinedundefinedPromise.all, and all other results are ignored.undefinedundefinedPromise.allSettled(promises) (recently added method) - waits for
all promises to settle and returns their results as an array of objects with:
undefinedundefinedstatus:
undefinedundefined"fulfilled" or undefinedundefined"rejected"undefinedundefinedvalue (if fulfilled) or undefinedundefinedreason
(if rejected).undefinedundefinedPromise.race(promises) - waits for the first promise to settle, and its result/error
becomes the outcome.undefinedundefinedPromise.any(promises) (recently added method) - waits for the first promise to
fulfill, and its result becomes the outcome. If all of the given promises are rejected, undefinedundefinedundefinedundefinedAggregateErrorundefinedundefined becomes the error
of undefinedundefinedPromise.any.undefinedundefinedPromise.resolve(value) - makes a resolved promise with the given
value.undefinedundefinedPromise.reject(error) - makes a
rejected promise with the given error.undefinedundefinedOf all
these, undefinedundefinedPromise.all is probably the most common in practice.undefinedundefined
Most of the time, a JavaScript application needs to work with information. Here are two examples: 1. An online shop - the information might include goods being sold and a shopping cart. 2. A chat application - the information might include users, messages, and much more.
undefinedundefinedVariables are used to store this information.
undefinedundefinedA undefinedundefinedvariable is a "named storage" for data. We can use variables to store goodies, visitors, and other data.undefinedundefined
undefinedundefinedTo create a
variable in JavaScript, use the undefinedundefinedlet keyword.undefinedundefined
The statement below creates (in other words: undefinedundefineddeclares) a variable with the name "message":undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet messageundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Now, we can put some data into it by using the assignment operator
undefinedundefined=:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet messageundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedmessage undefinedundefined=undefinedundefined'Hello'undefinedundefined;undefinedundefined// store the stringundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The string is now saved into the memory area associated with the variable. We can access it using the variable name:
undefinedundefinedrun let message; message = ‘Hello!''';
undefinedundefinedundefinedundefined! alert(message); // shows the variable content undefinedundefined/! undefinedundefined
undefinedundefinedTo be concise, we can combine the variable declaration and assignment into a single line:
undefinedundefinedrun let message = ‘Hello!'''; // define the variable and assign the value
undefinedundefinedalert(message); // Hello!
undefinedundefinedWe can also declare multiple variables in one line:
undefinedundefined
undefinedundefinedjs no-beautify let user = 'John', age = 25, message = 'Hello';undefinedundefined
That might seem shorter, but we don't recommend it. For the sake of better readability, please use a single line per variable.
undefinedundefinedThe multiline variant is a bit longer, but easier to read:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefined'John'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedlet age undefinedundefined=undefinedundefined25undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedlet message undefinedundefined=undefinedundefined'Hello'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Some people also define multiple variables in this multiline style:
undefinedundefinedjs no-beautify let user = 'John', age = 25, message = 'Hello';undefinedundefined
…Or even in the "comma-first" style:
undefinedundefined
undefinedundefinedjs no-beautify let user = 'John' , age = 25 , message = 'Hello';undefinedundefined
Technically, all these variants do the same thing. So, it's a matter of personal taste and aesthetics.
undefinedundefined
undefinedundefinedsmart header="varundefinedundefinedinstead ofletundefinedundefined" In older scripts, you may also find another keyword:varundefinedundefinedinstead oflet`:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedvarundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefined message = 'Hello';undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The undefinedundefinedvar keyword is undefinedundefinedalmost the same
as undefinedundefinedlet. It also declares a variable, but in a slightly different, "old-school"
way.undefinedundefined
There are subtle differences between undefinedundefinedlet
and undefinedundefinedvar, but they do not matter for us yet. We'll cover them in detail in the chapter
undefinedundefinedinfo:var.
undefinedundefined
We can easily grasp the concept of a "variable" if we imagine it as a "box" for data, with a uniquely-named sticker on it.
undefinedundefinedFor instance, the variable undefinedundefinedmessage can be imagined as a box
labeled undefinedundefined"message" with the value undefinedundefined"Hello!" in
it:undefinedundefined
undefinedundefinedundefinedundefined
We can put any value in the box.
undefinedundefinedWe can also change it as many times as we want: run let message;
undefinedundefinedmessage = ‘Hello!''';
undefinedundefinedmessage = ‘World!'''; // value changed
undefinedundefinedalert(message);
undefinedundefinedWhen the value is changed, the old data is removed from the variable:
undefinedundefined
undefinedundefinedundefinedundefined
We can also declare two variables and copy data from one into the other.
undefinedundefinedrun let hello = ‘Hello world!''';
undefinedundefinedlet message;
undefinedundefinedundefinedundefined! // copy ‘Hello world' from hello into message message = hello; undefinedundefined/!undefinedundefined
undefinedundefined// now two variables hold the same data alert(hello); // Hello world! alert(message); // Hello world!
undefinedundefinedwarn header="Declaring twice triggers an error" A variable should be declared only once.
undefinedundefinedA repeated declaration of the same variable is an error:
undefinedundefinedrun let message = "This";
undefinedundefined// repeated ‘let' leads to an error let message = "That"; // SyntaxError: ‘message' has already been declared
undefinedundefinedundefinedundefinedSo, we should declare a variable once and then refer to it without `let`.undefinedundefined
undefinedundefinedsmart header="Functional languages" It's interesting to note that there exist undefinedundefinedfunctional programming languages, like undefinedundefinedScala or undefinedundefinedErlang that forbid changing variable values.undefinedundefined
undefinedundefinedIn such languages, once the value is stored "in the box", it's there forever. If we need to store something else, the language forces us to create a new box (declare a new variable). We can't reuse the old one.
undefinedundefinedThough it may seem a little odd at first sight, these languages are quite capable of serious development. More than that, there are areas like parallel computations where this limitation confers certain benefits. Studying such a language (even if you're not planning to use it soon) is recommended to broaden the mind.
undefinedundefinedThere are two limitations on variable names in JavaScript:
undefinedundefined$ and
undefinedundefined_.undefinedundefinedExamples of valid names:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet userNameundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedlet test123undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
When the name contains multiple words, undefinedundefinedcamelCase is commonly used. That is: words go one after another,
each word except first starting with a capital letter:
undefinedundefinedmyVeryLongName.undefinedundefined
What's interesting - the
dollar sign undefinedundefined'$' and the underscore undefinedundefined'_' can also be used
in names. They are regular symbols, just like letters, without any special meaning.undefinedundefined
These names are valid:
undefinedundefinedrun untrusted let $ = 1; // declared a variable with the name "$" let _ = 2; // and now a variable with the name "_"
undefinedundefinedalert($ + _); // 3
undefinedundefinedExamples of incorrect variable names:
undefinedundefinedno-beautify let 1a; // cannot start with a digit
undefinedundefinedlet my-name; // hyphens ‘-''' aren't allowed in the name
undefinedundefined
undefinedundefinedsmart header="Case matters" Variables named `apple` and `AppLE` are two different variables.undefinedundefined
smart header="Non-Latin letters are allowed, but not recommended" It is possible to use any language, including cyrillic letters or even hieroglyphs, like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet имя undefinedundefined=undefinedundefined'...'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedlet 我 undefinedundefined=undefinedundefined'...'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Technically, there is no error here. Such names are allowed, but there is an international convention to use English in variable names. Even if we're writing a small script, it may have a long life ahead. People from other countries may need to read it some time.
undefinedundefinedwarn header="Reserved names" There is a undefinedundefinedlist of reserved words, which cannot be used as variable names because they are used by the language itself.undefinedundefined
undefinedundefinedFor example: undefinedundefinedlet, undefinedundefinedclass,
undefinedundefinedreturn, and undefinedundefinedfunction are reserved.undefinedundefined
The code below gives a syntax error:
undefinedundefined
undefinedundefinedjs run no-beautify let let = 5; // can't name a variable "let", error! let return = 5; // also can't name it "return", error!
undefinedundefined
undefinedundefinedwarn header="An assignment withoutuse strict`"undefinedundefined
Normally, we need to define a variable before using it. But in the old times, it was technically possible to create a
variable by a mere assignment of the value without using undefinedundefinedlet. This still works now if
we don't put undefinedundefineduse strict in our scripts to maintain compatibility with old
scripts.undefinedundefined
run no-strict // note: no "use strict" in this example
undefinedundefinednum = 5; // the variable "num" is created if it didn't exist
undefinedundefinedalert(num); // 5
undefinedundefinedThis is a bad practice and would cause an error in strict mode:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined"use strict"undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinednum undefinedundefined=undefinedundefined5undefinedundefined;undefinedundefined// error: num is not definedundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefined
To declare a constant (unchanging) variable,
use undefinedundefinedconst instead of undefinedundefinedlet:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedconst myBirthday undefinedundefined=undefinedundefined'18.04.1982'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Variables declared using undefinedundefinedconst are called "constants". They
cannot be reassigned. An attempt to do so would cause an error:undefinedundefined
run const myBirthday = ‘18.04.1982';
undefinedundefinedmyBirthday = ‘01.01.2001'; // error, can't reassign the constant!
undefinedundefinedWhen a programmer is sure that a variable will never change, they can declare it with
undefinedundefinedconst to guarantee and clearly communicate that fact to everyone.undefinedundefined
There is a widespread practice to use constants as aliases for difficult-to-remember values that are known prior to execution.
undefinedundefinedSuch constants are named using capital letters and underscores.
undefinedundefinedFor instance, let's make constants for colors in so-called "web" (hexadecimal) format:
undefinedundefinedrun const COLOR_RED = "#F00"; const COLOR_GREEN = "#0F0"; const COLOR_BLUE = "#00F"; const COLOR_ORANGE = "#FF7F00";
undefinedundefined// …when we need to pick a color let color = COLOR_ORANGE; alert(color); // #FF7F00
undefinedundefinedBenefits:
undefinedundefinedCOLOR_ORANGE is much easier to remember than
undefinedundefined"#FF7F00".undefinedundefined"#FF7F00" than undefinedundefinedCOLOR_ORANGE.undefinedundefinedCOLOR_ORANGE is much more meaningful than
undefinedundefined#FF7F00.undefinedundefinedWhen should we use capitals for a constant and when should we name it normally? Let's make that clear.
undefinedundefinedBeing a "constant" just means that a variable's value never changes. But there are constants that are known prior to execution (like a hexadecimal value for red) and there are constants that are undefinedundefinedcalculated in run-time, during the execution, but do not change after their initial assignment.undefinedundefined
undefinedundefinedFor instance:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedconst pageLoadTime undefinedundefined=undefinedundefined/* time taken by a webpage to load */undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The value of undefinedundefinedpageLoadTime is not known prior to the page load,
so it's named normally. But it's still a constant because it doesn't change after assignment.undefinedundefined
In other words, capital-named constants are only used as aliases for "hard-coded" values.
undefinedundefinedTalking about variables, there's one more extremely important thing.
undefinedundefinedA variable name should have a clean, obvious meaning, describing the data that it stores.
undefinedundefinedVariable naming is one of the most important and complex skills in programming. A quick glance at variable names can reveal which code was written by a beginner versus an experienced developer.
undefinedundefinedIn a real project, most of the time is spent modifying and extending an existing code base rather than writing something completely separate from scratch. When we return to some code after doing something else for a while, it's much easier to find information that is well-labeled. Or, in other words, when the variables have good names.
undefinedundefinedPlease spend time thinking about the right name for a variable before declaring it. Doing so will repay you handsomely.
undefinedundefinedSome good-to-follow rules are:
undefinedundefineduserName or
undefinedundefinedshoppingCart.undefinedundefineda, undefinedundefinedb,
undefinedundefinedc, unless you really know what you're doing.undefinedundefineddata and
undefinedundefinedvalue. Such names say nothing. It's only okay to use them if the context of the code
makes it exceptionally obvious which data or value the variable is referencing.undefinedundefinedcurrentUser or
undefinedundefinednewUser instead of undefinedundefinedcurrentVisitor or
undefinedundefinednewManInTown.undefinedundefinedSounds simple? Indeed it is, but creating descriptive and concise variable names in practice is not. Go for it.
undefinedundefinedsmart header="Reuse or create?" And the last note. There are some lazy programmers who, instead of declaring new variables, tend to reuse existing ones.
undefinedundefinedAs a result, their variables are like boxes into which people throw different things without changing their stickers. What's inside the box now? Who knows? We need to come closer and check.
undefinedundefinedSuch programmers save a little bit on variable declaration but lose ten times more on debugging.
undefinedundefinedAn extra variable is good, not evil.
undefinedundefinedModern JavaScript minifiers and browsers optimize code well enough, so it won't create performance issues. Using different variables for different values can even help the engine optimize your code.
undefinedundefinedWe can declare variables to store data by using
the undefinedundefinedvar, undefinedundefinedlet, or undefinedundefinedconst
keywords.undefinedundefined
let - is a
modern variable declaration.undefinedundefinedvar - is an
old-school variable declaration. Normally we don't use it at all, but we'll cover subtle differences from
undefinedundefinedlet in the chapter undefinedundefinedinfo:var,
just in case you need them.undefinedundefinedconst - is
like undefinedundefinedlet, but the value of the variable can't be changed.undefinedundefinedVariables should be named in a way that allows us to easily understand what's inside them.
undefinedundefined"Promisification" is a long word for a simple transformation. It's the conversion of a function that accepts a callback into a function that returns a promise.
undefinedundefinedSuch transformations are often required in real-life, as many functions and libraries are callback-based. But promises are more convenient, so it makes sense to promisify them.
undefinedundefinedFor better understanding, let's see an example.
undefinedundefinedFor
instance, we have undefinedundefinedloadScript(src, callback) from the chapter undefinedundefinedinfo:callbacks.undefinedundefined
run function loadScript(src, callback) { let script = document.createElement(‘script'); script.src = src;
undefinedundefined
script.onload = () => callback(null, script); script.onerror = () => callback(new
Error(undefinedundefinedScript load error for ${src}));undefinedundefined
document.head.append(script); }
undefinedundefined// usage: // loadScript(‘path/script.js', (err, script) => {…})
undefinedundefinedThe function loads a script with the given undefinedundefinedsrc, and then calls
undefinedundefinedcallback(err) in case of an error, or
undefinedundefinedcallback(null, script) in case of successful loading. That's a widespread agreement for
using callbacks, we saw it before.undefinedundefined
Let's promisify it.
undefinedundefinedWe'll make a new function undefinedundefinedloadScriptPromise(src), that does the same (loads the
script), but returns a promise instead of using callbacks.undefinedundefined
In other words,
we pass it only undefinedundefinedsrc (no undefinedundefinedcallback) and get a promise in
return, that resolves with undefinedundefinedscript when the load is successful, and rejects with the
error otherwise.undefinedundefined
Here it is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet loadScriptPromise undefinedundefined=undefinedundefinedfunction(src) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinednewundefinedundefinedPromise((resolveundefinedundefined, reject) undefinedundefined=>undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedloadScript(srcundefinedundefined, (errundefinedundefined, script) undefinedundefined=>undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (err) undefinedundefinedreject(err)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedelseundefinedundefinedresolve(script)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// usage:undefinedundefinedundefinedundefinedundefinedundefined// loadScriptPromise('path/script.js').then(...)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
As we can see, the new function is a wrapper around the original
undefinedundefinedloadScript function. It calls it providing its own callback that translates to promise
undefinedundefinedresolve/reject.undefinedundefined
Now
undefinedundefinedloadScriptPromise fits well in promise-based code. If we like promises more than
callbacks (and soon we'll see more reasons for that), then we will use it instead.undefinedundefined
In practice we may need to promisify more than one function, so it makes sense to use a helper.
undefinedundefinedWe'll call it undefinedundefinedpromisify(f): it accepts a to-promisify function
undefinedundefinedf and returns a wrapper function.undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedpromisify(f) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedfunction (...undefinedundefinedargs) undefinedundefined{undefinedundefined// return a wrapper-function (*)undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinednewundefinedundefinedPromise((resolveundefinedundefined, reject) undefinedundefined=>undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedcallback(errundefinedundefined, result) undefinedundefined{undefinedundefined// our custom callback for f (**)undefinedundefinedundefinedundefinedundefinedundefinedif (err) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreject(err)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedelseundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedresolve(result)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedargs.undefinedundefinedpush(callback)undefinedundefined;undefinedundefined// append our custom callback to the end of f argumentsundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedf.undefinedundefinedcall(undefinedundefinedthisundefinedundefined, ...undefinedundefinedargs)undefinedundefined;undefinedundefined// call the original functionundefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// usage:undefinedundefinedundefinedundefinedundefinedundefinedlet loadScriptPromise undefinedundefined=undefinedundefinedpromisify(loadScript)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedloadScriptPromise(...).undefinedundefinedthen(...)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The code may look a bit complex, but it's essentially the same that we wrote above, while
promisifying undefinedundefinedloadScript function.undefinedundefined
A call to
undefinedundefinedpromisify(f) returns a wrapper around
undefinedundefinedfundefinedundefined(*). That wrapper returns a promise and forwards the
call to the original undefinedundefinedf, tracking the result in the custom callback
undefinedundefined(**).undefinedundefined
Here,
undefinedundefinedpromisify assumes that the original function expects a callback with exactly two
arguments undefinedundefined(err, result). That's what we encounter most often. Then our custom callback
is in exactly the right format, and undefinedundefinedpromisify works great for such a
case.undefinedundefined
But what if the original undefinedundefinedf expects a
callback with more arguments undefinedundefinedcallback(err, res1, res2, ...)?undefinedundefined
We can improve our helper. Let's make a more advanced version of
undefinedundefinedpromisify.undefinedundefined
promisify(f) it should work similar to the version above.undefinedundefinedpromisify(f, true), it should return the promise
that resolves with the array of callback results. That's exactly for callbacks with many
arguments.undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// promisify(f, true) to get array of resultsundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedpromisify(fundefinedundefined, manyArgs undefinedundefined=undefinedundefinedfalse) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedfunction (...undefinedundefinedargs) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinednewundefinedundefinedPromise((resolveundefinedundefined, reject) undefinedundefined=>undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefined*!*undefinedundefinedcallback(errundefinedundefined, ...undefinedundefinedresultsundefinedundefined*undefinedundefined/!undefinedundefined*)undefinedundefined { // our custom callback for fundefinedundefinedundefinedundefinedundefinedundefined if undefinedundefined(undefinedundefinederrundefinedundefined)undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined rejectundefinedundefined(undefinedundefinederrundefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined } else {undefinedundefinedundefinedundefinedundefinedundefined // resolve with all callback results if manyArgs is specifiedundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined!undefinedundefined*undefinedundefinedresolveundefinedundefined(undefinedundefinedmanyArgs undefinedundefined?undefinedundefined results : resultsundefinedundefined[0])undefinedundefined;undefinedundefined*undefinedundefined/undefinedundefined!*undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedargs.undefinedundefinedpush(callback)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedf.undefinedundefinedcall(undefinedundefinedthisundefinedundefined, ...undefinedundefinedargs)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefined }undefinedundefined;undefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined// usage:undefinedundefinedundefinedundefinedf undefinedundefined=undefinedundefinedpromisify(fundefinedundefined,undefinedundefinedtrue)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedf(...).undefinedundefinedthen(arrayOfResults undefinedundefined=> ...undefinedundefined, err undefinedundefined=> ...)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
As you can see it's essentially the same as above, but undefinedundefinedresolve
is called with only one or all arguments depending on whether undefinedundefinedmanyArgs is
truthy.undefinedundefined
For more exotic callback formats, like those without
undefinedundefinederr at all: undefinedundefinedcallback(result), we can promisify such
functions manually without using the helper.undefinedundefined
There are also modules with a
bit more flexible promisification functions, e.g. undefinedundefinedes6-promisify. In Node.js, there's a built-in
undefinedundefinedutil.promisify function for that.undefinedundefined
undefinedundefinedPromisification is a great approach, especially when you use `async/await` (see the next chapter), but not a total replacement for callbacks.
Remember, a promise may have only one result, but a callback may technically be called many times.
So promisification is only meant for functions that call the callback once. Further calls will be ignored.undefinedundefined
undefinedundefinedPromise handlers
undefinedundefined.then/undefinedundefined.catch/undefinedundefined.finally are
always asynchronous.undefinedundefined
Even when a Promise is immediately resolved, the code
on the lines
undefinedundefinedbelowundefinedundefined.then/undefinedundefined.catch/undefinedundefined.finally
will still execute before these handlers.undefinedundefined
Here's a demo:
undefinedundefinedrun let promise = Promise.resolve();
undefinedundefinedpromise.then(() => alert("promise done!"));
undefinedundefinedalert("code finished"); // this alert shows first
undefinedundefinedIf you run it, you see undefinedundefinedcode finished first, and then
undefinedundefinedpromise done!.undefinedundefined
That's strange, because the promise is definitely done from the beginning.
undefinedundefinedWhy did the
undefinedundefined.then trigger afterwards? What's going on?undefinedundefined
Asynchronous tasks need proper management. For that,
the ECMA standard specifies an internal queue undefinedundefinedPromiseJobs, more often referred to as
the "microtask queue" (V8 term).undefinedundefined
As stated in the undefinedundefinedspecification:undefinedundefined
undefinedundefinedOr, to put it more simply, when a promise is ready, its
undefinedundefined.then/catch/finally handlers are put into the queue; they are not executed yet. When
the JavaScript engine becomes free from the current code, it takes a task from the queue and executes
it.undefinedundefined
That's why "code finished" in the example above shows first.
undefinedundefinedundefinedundefinedundefinedundefined
Promise handlers always go through this internal queue.
undefinedundefinedIf there's a chain with multiple
undefinedundefined.then/catch/finally, then every one of them is executed asynchronously. That is, it
first gets queued, then executed when the current code is complete and previously queued handlers are
finished.undefinedundefined
undefinedundefinedWhat if the order matters for us? How
can we make undefinedundefinedcode finished appear after
undefinedundefinedpromise done?undefinedundefinedundefinedundefined
Easy, just put it into the queue with undefinedundefined.then:undefinedundefined
undefinedundefinedjs run Promise.resolve() .then(() => alert("promise done!")) .then(() => alert("code finished"));undefinedundefined
Now the order is as intended.
undefinedundefinedRemember the undefinedundefinedunhandledrejection event from the
article undefinedundefinedinfo:promise-error-handling?undefinedundefined
Now we can see exactly how JavaScript finds out that there was an unhandled rejection.
undefinedundefinedundefinedundefinedAn "unhandled rejection" occurs when a promise error is not handled at the end of the microtask queue.undefinedundefined
undefinedundefinedNormally, if we expect an error, we add
undefinedundefined.catch to the promise chain to handle it:undefinedundefined
run let promise = Promise.reject(new Error("Promise Failed!")); undefinedundefined! promise.catch(err => alert(‘caught')); undefinedundefined/!undefinedundefined
undefinedundefined// doesn't run: error handled window.addEventListener(‘unhandledrejection', event => alert(event.reason));
undefinedundefinedBut if we forget to add undefinedundefined.catch, then, after the microtask queue
is empty, the engine triggers the event:undefinedundefined
run let promise = Promise.reject(new Error("Promise Failed!"));
undefinedundefined// Promise Failed! window.addEventListener(‘unhandledrejection', event => alert(event.reason));
undefinedundefinedWhat if we handle the error later? Like this:
undefinedundefinedrun let promise = Promise.reject(new Error("Promise Failed!")); undefinedundefined! setTimeout(() => promise.catch(err => alert(‘caught')), 1000); undefinedundefined/!undefinedundefined
undefinedundefined// Error: Promise Failed! window.addEventListener(‘unhandledrejection', event => alert(event.reason));
undefinedundefinedNow, if we run it, we'll see undefinedundefinedPromise Failed! first and then
undefinedundefinedcaught.undefinedundefined
If we didn't know about the
microtasks queue, we could wonder: "Why did undefinedundefinedunhandledrejection handler run? We did
catch and handle the error!"undefinedundefined
But now we understand that
undefinedundefinedunhandledrejection is generated when the microtask queue is complete: the engine
examines promises and, if any of them is in the "rejected" state, then the event triggers.undefinedundefined
In the example above, undefinedundefined.catch added by
undefinedundefinedsetTimeout also triggers. But it does so later, after
undefinedundefinedunhandledrejection has already occurred, so it doesn't change
anything.undefinedundefined
Promise handling is always asynchronous, as all promise actions pass through the internal "promise jobs" queue, also called "microtask queue" (V8 term).
undefinedundefinedSo undefinedundefined.then/catch/finally handlers are always
called after the current code is finished.undefinedundefined
If we need to guarantee that a
piece of code is executed after undefinedundefined.then/catch/finally, we can add it into a chained
undefinedundefined.then call.undefinedundefined
In most Javascript engines, including browsers and Node.js, the concept of microtasks is closely tied with the "event loop" and "macrotasks". As these have no direct relation to promises, they are covered in another part of the tutorial, in the article undefinedundefinedinfo:event-loop.undefinedundefined
undefinedundefinedThere's a special syntax to work with promises in a more comfortable fashion, called "async/await". It's surprisingly easy to understand and use.
undefinedundefinedLet's start with the
undefinedundefinedasync keyword. It can be placed before a function, like this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedasyncundefinedundefinedfunctionundefinedundefinedf() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefined1undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The word "async" before a function means one simple thing: a function always returns a promise. Other values are wrapped in a resolved promise automatically.
undefinedundefinedFor instance, this
function returns a resolved promise with the result of undefinedundefined1; let's test
it:undefinedundefined
run async function f() { return 1; }
undefinedundefinedf().then(alert); // 1
undefinedundefined…We could explicitly return a promise, which would be the same:
undefinedundefinedrun async function f() { return Promise.resolve(1); }
undefinedundefinedf().then(alert); // 1
undefinedundefinedSo, undefinedundefinedasync ensures that the function returns a promise, and wraps
non-promises in it. Simple enough, right? But not only that. There's another keyword,
undefinedundefinedawait, that works only inside undefinedundefinedasync functions, and it's
pretty cool.undefinedundefined
The syntax:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// works only inside async functionsundefinedundefinedundefinedundefinedundefinedundefinedlet value undefinedundefined=undefinedundefinedawait promiseundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The keyword undefinedundefinedawait makes JavaScript wait until that promise
settles and returns its result.undefinedundefined
Here's an example with a promise that resolves in 1 second: run async function f() {
undefinedundefinedlet promise = new Promise((resolve, reject) => { setTimeout(() => resolve("done!"), 1000) });
undefinedundefinedundefinedundefined! let result = await promise; // wait until the promise resolves (undefinedundefined) /!*undefinedundefined
undefinedundefinedalert(result); // "done!" }
undefinedundefinedf();
undefinedundefinedThe function execution "pauses" at the line undefinedundefined(*) and resumes when
the promise settles, with undefinedundefinedresult becoming its result. So the code above shows "done!"
in one second.undefinedundefined
Let's emphasize: undefinedundefinedawait
literally suspends the function execution until the promise settles, and then resumes it with the promise result. That
doesn't cost any CPU resources, because the JavaScript engine can do other jobs in the meantime: execute other
scripts, handle events, etc.undefinedundefined
It's just a more elegant syntax of getting the
promise result than undefinedundefinedpromise.then. And, it's easier to read and write.undefinedundefined
undefinedundefinedwarn header="Can't useawaitundefinedundefinedin regular functions" If we try to useawait`
in a non-async function, there would be a syntax error:undefinedundefined
undefinedundefinedjs run function f() { let promise = Promise.resolve(1); *!* let result = await promise; // Syntax error */!* }undefinedundefined
We may get this error if we forget to put undefinedundefinedasync before a
function. As stated earlier, undefinedundefinedawait only works inside an
undefinedundefinedasync function.
undefinedundefined
Let's take the undefinedundefinedshowAvatar() example from the
chapter undefinedundefinedinfo:promise-chaining and rewrite it using
undefinedundefinedasync/await:undefinedundefined
.then calls with
undefinedundefinedawait.undefinedundefinedasync for them to work.undefinedundefinedrun async function showAvatar() {
undefinedundefined// read our JSON let response = await fetch(‘/article/promise-chaining/user.json'); let user = await response.json();
undefinedundefined// read github
user let githubResponse = await fetch(undefinedundefinedhttps://api.github.com/users/${user.name}); let
githubUser = await githubResponse.json();undefinedundefined
// show the avatar let img = document.createElement(‘img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img);
undefinedundefined// wait 3 seconds await new Promise((resolve, reject) => setTimeout(resolve, 3000));
undefinedundefinedimg.remove();
undefinedundefinedreturn githubUser; }
undefinedundefinedshowAvatar();
undefinedundefinedPretty clean and easy to read, right? Much better than before.
undefinedundefined
undefinedundefinedsmart header="awaitundefinedundefinedwon't work in the top-level code" People who are just starting to useawaitundefinedundefinedtend to forget the fact that we can't useawait`
in top-level code. For example, this will not work:undefinedundefined
undefinedundefinedjs run // syntax error in top-level code let response = await fetch('/article/promise-chaining/user.json'); let user = await response.json();undefinedundefined
But we can wrap it into an anonymous async function, like this:
undefinedundefinedundefinedundefinedundefinedundefined(undefinedundefinedasync () undefinedundefined=>undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet response undefinedundefined=undefinedundefinedawaitundefinedundefinedfetch(undefinedundefined'/article/promise-chaining/user.json')undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedlet user undefinedundefined=undefinedundefinedawaitundefinedundefinedresponse.undefinedundefinedjson()undefinedundefined;undefinedundefinedundefinedundefined ...undefinedundefinedundefinedundefined})()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
P.S. New feature: starting from V8 engine version 8.9+, top-level await works in undefinedundefinedmodules. undefinedundefined
undefinedundefined
undefinedundefinedsmart header="awaitundefinedundefinedaccepts \"thenables\"" Likepromise.thenundefinedundefined,awaitundefinedundefinedallows us to use thenable objects (those with a callablethenundefinedundefinedmethod). The idea is that a third-party object may not be a promise, but promise-compatible: if it supports.thenundefinedundefined, that's enough to use it withawait`.undefinedundefined
Here's a demo undefinedundefinedThenable class; the
undefinedundefinedawait below accepts its instances:undefinedundefined
run class Thenable { constructor(num) { this.num = num; } then(resolve, reject) { alert(resolve); // resolve with this.numundefinedundefined2 after 1000ms setTimeout(() => resolve(this.num 2), 1000); // (*) } }undefinedundefined
undefinedundefinedasync function f() { // waits for 1 second, then result becomes 2 let result = await new Thenable(1); alert(result); }
undefinedundefinedf();
undefinedundefinedundefinedundefined
If `await` gets a non-promise object with `.then`, it calls that method providing the built-in functions `resolve` and `reject` as arguments (just as it does for a regular `Promise` executor). Then `await` waits until one of them is called (in the example above it happens in the line `(*)`) and then proceeds with the result.undefinedundefined
undefinedundefined
undefinedundefinedsmart header="Async class methods" To declare an async class method, just prepend it withasync`:undefinedundefined
run class Waiter { undefinedundefined! async wait() { undefinedundefined/! return await Promise.resolve(1); } }undefinedundefined
undefinedundefinednew Waiter() .wait() .then(alert); // 1 (this is the same as (result => alert(result)))
undefinedundefinedundefinedundefinedThe meaning is the same: it ensures that the returned value is a promise and enables `await`.
undefinedundefinedundefinedundefinedIf a
promise resolves normally, then undefinedundefinedawait promise returns the result. But in the case of a
rejection, it throws the error, just as if there were a undefinedundefinedthrow statement at that
line.undefinedundefined
This code:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedasyncundefinedundefinedfunctionundefinedundefinedf() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedawaitundefinedundefinedPromise.undefinedundefinedreject(undefinedundefinednewundefinedundefinedError(undefinedundefined"Whoops!"))undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…is the same as this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedasyncundefinedundefinedfunctionundefinedundefinedf() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedthrowundefinedundefinednewundefinedundefinedError(undefinedundefined"Whoops!")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In real situations, the promise may take some time before it rejects. In that case there will
be a delay before undefinedundefinedawait throws an error.undefinedundefined
We
can catch that error using undefinedundefinedtry..catch, the same way as a regular
undefinedundefinedthrow:undefinedundefined
run async function f() {
undefinedundefinedtry { let response = await fetch(‘http://no-such-url'); } catch(err) { undefinedundefined! alert(err); // TypeError: failed to fetch undefinedundefined/! } }undefinedundefined
undefinedundefinedf();
undefinedundefinedIn the case of an error, the control jumps to the undefinedundefinedcatch block.
We can also wrap multiple lines:undefinedundefined
run async function f() {
undefinedundefinedtry { let response = await fetch(‘/no-user-here'); let user = await response.json(); } catch(err) { // catches errors both in fetch and response.json alert(err); } }
undefinedundefinedf();
undefinedundefinedIf we don't have undefinedundefinedtry..catch, then the promise generated by the
call of the async function undefinedundefinedf() becomes rejected. We can append
undefinedundefined.catch to handle it:undefinedundefined
run async function f() { let response = await fetch(‘http://no-such-url'); }
undefinedundefined// f() becomes a rejected promise undefinedundefined! f().catch(alert); // TypeError: failed to fetch // (undefinedundefined) /!* undefinedundefined
undefinedundefinedIf we forget to add undefinedundefined.catch there, then we
get an unhandled promise error (viewable in the console). We can catch such errors using a global
undefinedundefinedunhandledrejection event handler as described in the chapter undefinedundefinedinfo:promise-error-handling.undefinedundefined
``undefinedundefinedsmart header="async/awaitundefinedundefinedandpromise.then/catchundefinedundefined" When we useasync/awaitundefinedundefined, we rarely need.thenundefinedundefined, becauseawaitundefinedundefinedhandles the waiting for us. And we can use a regulartry..catchundefinedundefinedinstead of.catch`.
That's usually (but not always) more convenient.undefinedundefined
But at the top level of the
code, when we're outside any undefinedundefinedasync function, we're syntactically unable to use
undefinedundefinedawait, so it's a normal practice to add undefinedundefined.then/catch to
handle the final result or falling-through error, like in the line undefinedundefined(*) of the example
above.
undefinedundefined
undefinedundefinedsmart header="async/awaitundefinedundefinedworks well withPromise.allundefinedundefined" When we need to wait for multiple promises, we can wrap them inPromise.allundefinedundefinedand thenawait`:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// wait for the array of resultsundefinedundefinedundefinedundefinedundefinedundefinedlet results undefinedundefined=undefinedundefinedawaitundefinedundefinedPromise.undefinedundefinedall([undefinedundefinedundefinedundefinedundefinedundefinedfetch(url1)undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedfetch(url2)undefinedundefined,undefinedundefinedundefinedundefined ...undefinedundefined])undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In the case of an error, it propagates as usual, from the failed promise to
undefinedundefinedPromise.all, and then becomes an exception that we can catch using
undefinedundefinedtry..catch around the call.undefinedundefined
undefinedundefined
The undefinedundefinedasync keyword
before a function has two effects:undefinedundefined
await to be used in
it.undefinedundefinedThe undefinedundefinedawait
keyword before a promise makes JavaScript wait until that promise settles, and then:undefinedundefined
throw error were called at that very place.undefinedundefinedTogether they provide a great framework to write asynchronous code that is easy to both read and write.
undefinedundefinedWith
undefinedundefinedasync/await we rarely need to write undefinedundefinedpromise.then/catch,
but we still shouldn't forget that they are based on promises, because sometimes (e.g. in the outermost scope) we have
to use these methods. Also undefinedundefinedPromise.all is nice when we are waiting for many tasks
simultaneously.undefinedundefined
Regular functions return only one, single value (or nothing).
undefinedundefinedGenerators can return ("yield") multiple values, one after another, on-demand. They work great with undefinedundefinediterables, allowing to create data streams with ease.undefinedundefined
undefinedundefinedTo create a generator, we need a special syntax construct:
undefinedundefinedfunction*, so-called "generator function".undefinedundefined
It looks like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefined*undefinedundefinedgenerateSequence() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedyieldundefinedundefined1undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedyieldundefinedundefined2undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefined3undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Generator functions behave differently from regular ones. When such function is called, it doesn't run its code. Instead it returns a special object, called "generator object", to manage the execution.
undefinedundefinedHere, take a look:
undefinedundefinedrun function* generateSequence() { yield 1; yield 2; return 3; }
undefinedundefined// "generator function" creates "generator object" let generator = generateSequence(); undefinedundefined! alert(generator); // [object Generator] undefinedundefined/! undefinedundefined
undefinedundefinedThe function code execution hasn't started yet:
undefinedundefined
undefinedundefinedundefinedundefined
The main method of a
generator is undefinedundefinednext(). When called, it runs the execution until the nearest
undefinedundefinedyield <value> statement (undefinedundefinedvalue can be omitted,
then it's undefinedundefinedundefined). Then the function execution pauses, and the yielded
undefinedundefinedvalue is returned to the outer code.undefinedundefined
The
result of undefinedundefinednext() is always an object with two properties: -
undefinedundefinedvalue: the yielded value. - undefinedundefineddone:
undefinedundefinedtrue if the function code has finished, otherwise
undefinedundefinedfalse.undefinedundefined
For instance, here we create the generator and get its first yielded value:
undefinedundefinedrun function* generateSequence() { yield 1; yield 2; return 3; }
undefinedundefinedlet generator = generateSequence();
undefinedundefinedundefinedundefined! let one = generator.next(); undefinedundefined/!undefinedundefined
undefinedundefinedalert(JSON.stringify(one)); // {value: 1, done: false}
undefinedundefinedAs of now, we got the first value only, and the function execution is on the second line:
undefinedundefinedundefinedundefinedundefinedundefined
Let's call undefinedundefinedgenerator.next() again. It resumes the code execution and returns the next
undefinedundefinedyield:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet two undefinedundefined=undefinedundefinedgenerator.undefinedundefinednext()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefinedJSON.undefinedundefinedstringify(two))undefinedundefined;undefinedundefined// {value: 2, done: false}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedundefinedundefined
And, if we call it a third time, the execution reaches the undefinedundefinedreturn
statement that finishes the function:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet three undefinedundefined=undefinedundefinedgenerator.undefinedundefinednext()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefinedJSON.undefinedundefinedstringify(three))undefinedundefined;undefinedundefined// {value: 3, *!*done: true*/!*}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedundefinedundefined
Now the generator is done. We should see it from undefinedundefineddone:true and
process undefinedundefinedvalue:3 as the final result.undefinedundefined
New
calls to undefinedundefinedgenerator.next() don't make sense any more. If we do them, they return the
same object: undefinedundefined{done: true}.undefinedundefined
``undefinedundefinedsmart header="function* f(…)undefinedundefinedorfunction *f(…)`?" Both
syntaxes are correct.undefinedundefined
But usually the first syntax is preferred, as the star
undefinedundefined* denotes that it's a generator function, it describes the kind, not the name, so it
should stick with the undefinedundefinedfunction keyword.
undefinedundefined
As you probably already guessed looking at the undefinedundefinednext() method,
generators are undefinedundefinediterable.undefinedundefined
We
can loop over their values using undefinedundefinedfor..of:undefinedundefined
run function* generateSequence() { yield 1; yield 2; return 3; }
undefinedundefinedlet generator = generateSequence();
undefinedundefinedfor(let value of generator) { alert(value); // 1, then 2 }
undefinedundefinedLooks a lot nicer than calling undefinedundefined.next().value,
right?undefinedundefined
…But please note: the example above shows
undefinedundefined1, then undefinedundefined2, and that's all. It doesn't show
undefinedundefined3!undefinedundefined
It's because
undefinedundefinedfor..of iteration ignores the last undefinedundefinedvalue, when
undefinedundefineddone: true. So, if we want all results to be shown by
undefinedundefinedfor..of, we must return them with
undefinedundefinedyield:undefinedundefined
run function* generateSequence() { yield 1; yield 2; undefinedundefined! yield 3; undefinedundefined/! }undefinedundefined
undefinedundefinedlet generator = generateSequence();
undefinedundefinedfor(let value of generator) { alert(value); // 1, then 2, then 3 }
undefinedundefinedAs generators are iterable, we can call all related functionality, e.g. the spread syntax
undefinedundefined...:undefinedundefined
run function* generateSequence() { yield 1; yield 2; yield 3; }
undefinedundefinedlet sequence = [0, …generateSequence()];
undefinedundefinedalert(sequence); // 0, 1, 2, 3
undefinedundefinedIn the code above, undefinedundefined...generateSequence() turns the iterable
generator object into an array of items (read more about the spread syntax in the chapter undefinedundefined)undefinedundefined
Some time ago, in the
chapter undefinedundefined we created an iterable undefinedundefinedrange
object that returns values undefinedundefinedfrom..to.undefinedundefined
Here, let's remember the code:
undefinedundefinedrun let range = { from: 1, to: 5,
undefinedundefined// for..of range calls this method once in the very beginning undefinedundefinedSymbol.iterator { // …it returns the iterator object: // onward, for..of works only with that object, asking it for next values return { current: this.from, last: this.to,undefinedundefined
undefinedundefinedundefinedundefined // next() is called on each iteration by the for..of loop
next() {
// it should return the value as an object {done:.., value :...}
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};undefinedundefinedundefinedundefined} };
undefinedundefined// iteration over range returns numbers from range.from to range.to alert([…range]); // 1,2,3,4,5
undefinedundefinedWe can use a generator function for iteration by providing it as
undefinedundefinedSymbol.iterator.undefinedundefined
Here's the same
undefinedundefinedrange, but much more compact:undefinedundefined
run let range = { from: 1, to: 5,
undefinedundefinedundefinedundefinedundefinedundefinedSymbol.iterator { // a shorthand for [Symbol.iterator]: functionundefinedundefined() for(let value = this.from; value <= this.to; value++) { yield value; } } };undefinedundefined
undefinedundefinedalert( […range] ); // 1,2,3,4,5
undefinedundefinedThat works, because undefinedundefinedrange[Symbol.iterator]() now returns a
generator, and generator methods are exactly what undefinedundefinedfor..of expects: - it has a
undefinedundefined.next() method - that returns values in the form
undefinedundefined{value: ..., done: true/false}undefinedundefined
That's not a coincidence, of course. Generators were added to JavaScript language with iterators in mind, to implement them easily.
undefinedundefinedThe variant with a generator is much more concise than the original iterable code of
undefinedundefinedrange, and keeps the same functionality.undefinedundefined
smart header="Generators may generate values forever" In the examples above we generated finite sequences, but we can also make a generator that yields values forever. For instance, an unending sequence of pseudo-random numbers.
undefinedundefinedThat surely would require a undefinedundefinedbreak (or
undefinedundefinedreturn) in undefinedundefinedfor..of over such generator. Otherwise, the
loop would repeat forever and hang.
undefinedundefined
Generator composition is a special feature of generators that allows to transparently "embed" generators in each other.
undefinedundefinedFor instance, we have a function that generates a sequence of numbers:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefined*undefinedundefinedgenerateSequence(startundefinedundefined, end) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedfor (undefinedundefinedlet i undefinedundefined= startundefinedundefined; i undefinedundefined<= endundefinedundefined; iundefinedundefined++) undefinedundefinedyield iundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Now we'd like to reuse it to generate a more complex sequence: - first, digits
undefinedundefined0..9 (with character codes 48..57), - followed by uppercase alphabet letters
undefinedundefinedA..Z (character codes 65..90) - followed by lowercase alphabet letters
undefinedundefineda..z (character codes 97..122)undefinedundefined
We can use this sequence e.g. to create passwords by selecting characters from it (could add syntax characters as well), but let's generate it first.
undefinedundefinedIn a regular function, to combine results from multiple other functions, we call them, store the results, and then join at the end.
undefinedundefinedFor generators, there's
a special undefinedundefinedyield* syntax to "embed" (compose) one generator into
another.undefinedundefined
The composed generator:
undefinedundefinedrun function* generateSequence(start, end) { for (let i = start; i <= end; i++) yield i; }
undefinedundefinedfunction* generatePasswordCodes() {
undefinedundefinedundefinedundefined! // 0..9 yield* generateSequence(48, 57);undefinedundefined
undefinedundefined// A..Z yield* generateSequence(65, 90);
undefinedundefined// a..z yield* generateSequence(97, 122); undefinedundefined/!undefinedundefined
undefinedundefined}
undefinedundefinedlet str = '';
undefinedundefinedfor(let code of generatePasswordCodes()) { str += String.fromCharCode(code); }
undefinedundefinedalert(str); // 0..9A..Za..z
undefinedundefinedThe undefinedundefinedyield* directive undefinedundefineddelegates the
execution to another generator. This term means that undefinedundefinedyield* gen iterates over the
generator undefinedundefinedgen and transparently forwards its yields outside. As if the values were
yielded by the outer generator.undefinedundefined
The result is the same as if we inlined the code from nested generators:
undefinedundefinedrun function* generateSequence(start, end) { for (let i = start; i <= end; i++) yield i; }
undefinedundefinedfunction* generateAlphaNum() {
undefinedundefinedundefinedundefined! // yield* generateSequence(48, 57); for (let i = 48; i <= 57; i++) yield i;undefinedundefined
undefinedundefined// yield* generateSequence(65, 90); for (let i = 65; i <= 90; i++) yield i;
undefinedundefined// yield* generateSequence(97, 122); for (let i = 97; i <= 122; i++) yield i; undefinedundefined/!undefinedundefined
undefinedundefined}
undefinedundefinedlet str = '';
undefinedundefinedfor(let code of generateAlphaNum()) { str += String.fromCharCode(code); }
undefinedundefinedalert(str); // 0..9A..Za..z
undefinedundefinedA generator composition is a natural way to insert a flow of one generator into another. It doesn't use extra memory to store intermediate results.
undefinedundefinedUntil this moment, generators were similar to iterable objects, with a special syntax to generate values. But in fact they are much more powerful and flexible.
undefinedundefinedThat's because undefinedundefinedyield is a two-way street: it not only returns the result to the
outside, but also can pass the value inside the generator.undefinedundefined
To do so, we
should call undefinedundefinedgenerator.next(arg), with an argument. That argument becomes the result of
undefinedundefinedyield.undefinedundefined
Let's see an example:
undefinedundefinedrun function* gen() { undefinedundefined! // Pass a question to the outer code and wait for an answer let result = yield "2 + 2 = ?"; // (undefinedundefined) /!*undefinedundefined
undefinedundefinedalert(result); }
undefinedundefinedlet generator = gen();
undefinedundefinedlet question = generator.next().value; // <- yield returns the value
undefinedundefinedgenerator.next(4); //
-> pass the result into the generatorundefinedundefined
undefinedundefined
undefinedundefinedundefinedundefined
generator.next()
should be always made without an argument (the argument is ignored if passed). It starts the execution and returns
the result of the first undefinedundefinedyield "2+2=?". At this point the generator pauses the
execution, while staying on the line undefinedundefined(*).undefinedundefinedyield gets into the
undefinedundefinedquestion variable in the calling code.undefinedundefinedgenerator.next(4), the generator resumes, and undefinedundefined4 gets
in as the result: undefinedundefinedlet result = 4.undefinedundefinedPlease note, the outer code does not have to immediately call
undefinedundefinednext(4). It may take time. That's not a problem: the generator will
wait.undefinedundefined
For instance:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// resume the generator after some timeundefinedundefinedundefinedundefinedundefinedundefinedsetTimeout(() undefinedundefined=>undefinedundefinedgenerator.undefinedundefinednext(undefinedundefined4)undefinedundefined,undefinedundefined1000)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
As we can see, unlike regular functions, a generator and the calling code can exchange
results by passing values in undefinedundefinednext/yield.undefinedundefined
To make things more obvious, here's another example, with more calls:
undefinedundefinedrun function* gen() { let ask1 = yield "2 + 2 = ?";
undefinedundefinedalert(ask1); // 4
undefinedundefinedlet ask2 = yield "3 * 3 = ?"
undefinedundefinedalert(ask2); // 9 }
undefinedundefinedlet generator = gen();
undefinedundefinedalert( generator.next().value ); // "2 + 2 = ?"
undefinedundefinedalert( generator.next(4).value ); // "3 * 3 = ?"
undefinedundefinedalert( generator.next(9).done ); // true
undefinedundefinedThe execution picture:
undefinedundefinedundefinedundefinedundefinedundefined
.next() starts the execution… It reaches the first
undefinedundefinedyield.undefinedundefined.next(4) passes
undefinedundefined4 back to the generator as the result of the first
undefinedundefinedyield, and resumes the execution.undefinedundefinedyield, that becomes the result of the generator
call.undefinedundefinednext(9) passes
undefinedundefined9 into the generator as the result of the second undefinedundefinedyield
and resumes the execution that reaches the end of the function, so
undefinedundefineddone: true.undefinedundefinedIt's
like a "ping-pong" game. Each undefinedundefinednext(value) (excluding the first one) passes a value into
the generator, that becomes the result of the current undefinedundefinedyield, and then gets back the
result of the next undefinedundefinedyield.undefinedundefined
As we observed in the examples above, the outer code may
pass a value into the generator, as the result of undefinedundefinedyield.undefinedundefined
…But it can also initiate (throw) an error there. That's natural, as an error is a kind of result.
undefinedundefinedTo pass an error into a undefinedundefinedyield, we should call
undefinedundefinedgenerator.throw(err). In that case, the undefinedundefinederr is thrown in
the line with that undefinedundefinedyield.undefinedundefined
For instance, here
the yield of undefinedundefined"2 + 2 = ?" leads to an error:undefinedundefined
run function* gen() { try { let result = yield "2 + 2 = ?"; // (1)
undefinedundefinedundefinedundefinedalert("The execution does not reach here, because the exception is thrown above");undefinedundefined
undefinedundefined} catch(e) { alert(e); // shows the error } }
undefinedundefinedlet generator = gen();
undefinedundefinedlet question = generator.next().value;
undefinedundefinedundefinedundefined! generator.throw(new Error("The answer is not found in my database")); // (2) undefinedundefined/! undefinedundefined
undefinedundefinedThe error, thrown into the generator at line
undefinedundefined(2) leads to an exception in line undefinedundefined(1) with
undefinedundefinedyield. In the example above, undefinedundefinedtry..catch catches it and
shows it.undefinedundefined
If we don't catch it, then just like any exception, it "falls out" the generator into the calling code.
undefinedundefinedThe current line of the calling code is the line with
undefinedundefinedgenerator.throw, labelled as undefinedundefined(2). So we can catch it
here, like this:undefinedundefined
run function* generate() { let result = yield "2 + 2 = ?"; // Error in this line }
undefinedundefinedlet generator = generate();
undefinedundefinedlet question = generator.next().value;
undefinedundefinedundefinedundefined! try { generator.throw(new Error("The answer is not found in my database")); } catch(e) { alert(e); // shows the error } undefinedundefined/! undefinedundefined
undefinedundefinedIf we don't catch the error there, then, as usual, it falls through to the outer calling code (if any) and, if uncaught, kills the script.
undefinedundefinedfunction* f(…) {…}.undefinedundefinedyield operator.undefinedundefinednext/yield
calls.undefinedundefinedIn modern JavaScript, generators are rarely used. But sometimes they come in handy, because the ability of a function to exchange data with the calling code during the execution is quite unique. And, surely, they are great for making iterable objects.
undefinedundefinedAlso, in the next chapter we'll learn async generators, which are used to read streams of asynchronously generated
data (e.g paginated fetches over a network) in undefinedundefinedfor await ... of
loops.undefinedundefined
In web-programming we often work with streamed data, so that's another very important use case.
undefinedundefinedAsynchronous iteration allow us to iterate over data that comes asynchronously, on-demand. Like, for instance, when we download something chunk-by-chunk over a network. And asynchronous generators make it even more convenient.
undefinedundefinedLet's see a simple example first, to grasp the syntax, and then review a real-life use case.
undefinedundefinedLet's recall the topic about iterables.
undefinedundefinedThe idea is that we have an object, such as
undefinedundefinedrange here:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet range undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedfromundefinedundefined:undefinedundefined1undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedtoundefinedundefined:undefinedundefined5undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…And we'd like to use undefinedundefinedfor..of loop on it, such as
undefinedundefinedfor(value of range), to get values from undefinedundefined1 to
undefinedundefined5.undefinedundefined
In other words, we want to add an undefinedundefinediteration ability to the object.undefinedundefined
undefinedundefinedThat can be
implemented using a special method with the name undefinedundefinedSymbol.iterator:undefinedundefined
for..of
construct when the loop is started, and it should return an object with the undefinedundefinednext
method.undefinedundefinednext()
method is invoked for the next value.undefinedundefinednext() should return a value in the form
undefinedundefined{done: true/false, value:<loop value>}, where
undefinedundefineddone:true means the end of the loop.undefinedundefinedHere's an implementation for the iterable undefinedundefinedrange:undefinedundefined
run let range = { from: 1, to: 5,
undefinedundefinedundefinedundefined!undefinedundefinedSymbol.iterator { // called once, in the beginning of for..of undefinedundefined/! return { current: this.from, last: this.to,undefinedundefined
undefinedundefinedundefinedundefined! next() { // called every iteration, to get the next value undefinedundefined/! if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; } };undefinedundefined
undefinedundefinedfor(let value of range) { alert(value); // 1 then 2, then 3, then 4, then 5 }
undefinedundefinedIf anything is unclear, please visit the chapter undefinedundefined, it gives all the details about regular iterables.undefinedundefined
undefinedundefinedAsynchronous iteration is needed when values come
asynchronously: after undefinedundefinedsetTimeout or another kind of delay.undefinedundefined
The most common case is that the object needs to make a network request to deliver the next value, we'll see a real-life example of it a bit later.
undefinedundefinedTo make an object iterable asynchronously:
undefinedundefinedSymbol.asyncIterator instead
of undefinedundefinedSymbol.iterator.undefinedundefinednext() method should return a promise (to be fulfilled with the next value).
undefinedundefinedasync keyword handles it, we can
simply make undefinedundefinedasync next().undefinedundefinedfor await (let item of iterable) loop.
undefinedundefinedawait word.undefinedundefined
As a starting
example, let's make an iterable undefinedundefinedrange object, similar like the one before, but now it
will return values asynchronously, one per second.undefinedundefined
All we need to do is to perform a few replacements in the code above:
undefinedundefinedrun let range = { from: 1, to: 5,
undefinedundefinedundefinedundefined!undefinedundefinedSymbol.asyncIterator { // (1) undefinedundefined/! return { current: this.from, last: this.to,undefinedundefined
undefinedundefinedundefinedundefined! async next() { // (2) undefinedundefined/!undefinedundefined
undefinedundefinedundefinedundefined! // note: we can use "await" inside the async next: await new Promise(resolve => setTimeout(resolve, 1000)); // (3) undefinedundefined/!undefinedundefined
undefinedundefinedundefinedundefined if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};undefinedundefinedundefinedundefined} };
undefinedundefined(async () => {
undefinedundefinedundefinedundefined! for await (let value of range) { // (4) alert(value); // 1,2,3,4,5 } undefinedundefined/!undefinedundefined
undefinedundefined})()
undefinedundefinedAs we can see, the structure is similar to regular iterators:
undefinedundefinedSymbol.asyncIteratorundefinedundefined(1).undefinedundefinednext() method returning a
promise undefinedundefined(2).undefinedundefinednext() method doesn't have to be undefinedundefinedasync, it may be a
regular method returning a promise, but undefinedundefinedasync allows us to use
undefinedundefinedawait, so that's convenient. Here we just delay for a second
undefinedundefined(3).undefinedundefinedfor await(let value of range)undefinedundefined(4), namely add "await"
after "for". It calls undefinedundefinedrange[Symbol.asyncIterator]() once, and then its
undefinedundefinednext() for values.undefinedundefinedHere's a small table with the differences:
undefinedundefined| undefinedundefined | Iterators | undefinedundefinedAsync iterators | undefinedundefined
|---|---|---|
| Object method to provide iterator | undefinedundefined
undefinedundefinedSymbol.iteratorundefinedundefined | undefinedundefined
undefinedundefinedSymbol.asyncIteratorundefinedundefined | undefinedundefined
undefinedundefinednext() return value
isundefinedundefined | undefinedundefinedany value | undefinedundefined
undefinedundefinedPromiseundefinedundefined | undefinedundefined
| to loop, use | undefinedundefined
undefinedundefinedfor..ofundefinedundefined | undefinedundefined
undefinedundefinedfor await..ofundefinedundefined | undefinedundefined
undefinedundefinedwarn header="The spread syntax…` doesn't work asynchronously" Features that require
regular, synchronous iterators, don't work with asynchronous ones.undefinedundefined
For instance, a spread syntax won't work:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedalert( [...undefinedundefinedrange] )undefinedundefined;undefinedundefined// Error, no Symbol.iteratorundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That's natural, as it expects to find undefinedundefinedSymbol.iterator, not
undefinedundefinedSymbol.asyncIterator.undefinedundefined
It's also the case for
undefinedundefinedfor..of: the syntax without undefinedundefinedawait needs
undefinedundefinedSymbol.iterator.
undefinedundefined
Now let's recall generators, as they allow to make iteration code much shorter. Most of the time, when we'd like to make an iterable, we'll use generators.
undefinedundefinedFor sheer simplicity, omitting some important stuff, they are "functions that generate (yield) values". They are explained in detail in the chapter undefinedundefined.undefinedundefined
undefinedundefinedGenerators are labelled with
undefinedundefinedfunction* (note the star) and use undefinedundefinedyield to generate a
value, then we can use undefinedundefinedfor..of to loop over them.undefinedundefined
This example generates a sequence of values from undefinedundefinedstart to
undefinedundefinedend:undefinedundefined
run function* generateSequence(start, end) { for (let i = start; i <= end; i++) { yield i; } }
undefinedundefinedfor(let value of generateSequence(1, 5)) { alert(value); // 1, then 2, then 3, then 4, then 5 }
undefinedundefinedAs we already know, to make an object iterable, we should add
undefinedundefinedSymbol.iterator to it.undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet range undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedfromundefinedundefined:undefinedundefined1undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedtoundefinedundefined:undefinedundefined5undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefined [undefinedundefinedSymbol.undefinedundefinediterator]() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefined<object undefinedundefinedwith next to make range iterableundefinedundefined>undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
A common practice for undefinedundefinedSymbol.iterator is to return a
generator, it makes the code shorter, as you can see:undefinedundefined
run let range = { from: 1, to: 5,
undefinedundefinedundefinedundefinedundefinedundefinedSymbol.iterator { // a shorthand for [Symbol.iterator]: functionundefinedundefined() for(let value = this.from; value <= this.to; value++) { yield value; } } };undefinedundefined
undefinedundefinedfor(let value of range) { alert(value); // 1, then 2, then 3, then 4, then 5 }
undefinedundefinedPlease see the chapter undefinedundefined if you'd like more details.undefinedundefined
undefinedundefinedIn regular generators we can't use
undefinedundefinedawait. All values must come synchronously, as required by the
undefinedundefinedfor..of construct.undefinedundefined
What if we'd like to generate values asynchronously? From network requests, for instance.
undefinedundefinedLet's switch to asynchronous generators to make it possible.
undefinedundefinedFor most practical applications, when we'd like to make an object that asynchronously generates a sequence of values, we can use an asynchronous generator.
undefinedundefinedThe
syntax is simple: prepend undefinedundefinedfunction* with undefinedundefinedasync. That
makes the generator asynchronous.undefinedundefined
And then use
undefinedundefinedfor await (...) to iterate over it, like this:undefinedundefined
run undefinedundefined!asyncundefinedundefined/! function* generateSequence(start, end) {undefinedundefined
undefinedundefinedfor (let i = start; i <= end; i++) {
undefinedundefinedundefinedundefined! // Wow, can use await! await new Promise(resolve => setTimeout(resolve, 1000)); undefinedundefined/!undefinedundefined
undefinedundefinedundefinedundefinedyield i;undefinedundefinedundefinedundefined}
undefinedundefined}
undefinedundefined(async () => {
undefinedundefinedlet generator = generateSequence(1, 5); for undefinedundefined!awaitundefinedundefined/! (let value of generator) { alert(value); // 1, then 2, then 3, then 4, then 5 (with delay between) }undefinedundefined
undefinedundefined})();
undefinedundefinedAs the generator is asynchronous, we can use undefinedundefinedawait inside it,
rely on promises, perform network requests and so on.undefinedundefined
smart header="Under-the-hood difference" Technically, if you're an advanced reader who remembers the details about generators, there's an internal difference.
undefinedundefinedFor async generators, the
undefinedundefinedgenerator.next() method is asynchronous, it returns promises.undefinedundefined
In a regular generator we'd use undefinedundefinedresult = generator.next() to get
values. In an async generator, we should add undefinedundefinedawait, like this:undefinedundefined
undefinedundefinedundefinedundefinedresult undefinedundefined=undefinedundefinedawaitundefinedundefinedgenerator.undefinedundefinednext()undefinedundefined;undefinedundefined// result = {value: ..., done: true/false}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That's why async generators work with undefinedundefinedfor await...of.
undefinedundefined
Regular generators can be used as undefinedundefinedSymbol.iterator to make the iteration code
shorter.undefinedundefined
Similar to that, async generators can be used as
undefinedundefinedSymbol.asyncIterator to implement the asynchronous iteration.undefinedundefined
For instance, we can make the undefinedundefinedrange object generate values
asynchronously, once per second, by replacing synchronous undefinedundefinedSymbol.iterator with
asynchronous undefinedundefinedSymbol.asyncIterator:undefinedundefined
run let range = { from: 1, to: 5,
undefinedundefined// this line is same as [Symbol.asyncIterator]: async functionundefinedundefined() { !undefinedundefined async undefinedundefinedSymbol.asyncIterator { undefinedundefined/! for(let value = this.from; value <= this.to; value++) {undefinedundefined
undefinedundefinedundefinedundefined // make a pause between values, wait for something
await new Promise(resolve => setTimeout(resolve, 1000));
yield value;
}undefinedundefinedundefinedundefined} };
undefinedundefined(async () => {
undefinedundefinedfor undefinedundefined!awaitundefinedundefined/! (let value of range) { alert(value); // 1, then 2, then 3, then 4, then 5 }undefinedundefined
undefinedundefined})();
undefinedundefinedNow values come with a delay of 1 second between them.
undefinedundefinedundefinedundefinedTechnically, we can add both `Symbol.iterator` and `Symbol.asyncIterator` to the object, so it's both synchronously (`for..of`) and asynchronously (`for await..of`) iterable.
In practice though, that would be a weird thing to do.undefinedundefinedundefinedundefinedSo far we've seen basic examples, to gain understanding. Now let's review a real-life use case.
undefinedundefinedThere are many online services that deliver paginated data. For instance, when we need a list of users, a request returns a pre-defined count (e.g. 100 users) - "one page", and provides a URL to the next page.
undefinedundefinedThis pattern is very common. It's not about users, but just about anything.
undefinedundefinedFor instance, GitHub allows us to retrieve commits in the same, paginated fashion:
undefinedundefinedfetch in the form
undefinedundefinedhttps://api.github.com/repos/<repo>/commits.undefinedundefinedLink header.undefinedundefinedFor our code, we'd like to have a simpler way to get commits.
undefinedundefinedLet's make a function
undefinedundefinedfetchCommits(repo) that gets commits for us, making requests whenever needed. And let
it care about all pagination stuff. For us it'll be a simple async iteration
undefinedundefinedfor await..of.undefinedundefined
So the usage will be like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedforundefinedundefinedawait (undefinedundefinedlet commit undefinedundefinedofundefinedundefinedfetchCommits(undefinedundefined"username/repository")) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// process commitundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Here's such function, implemented as async generator:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedasyncundefinedundefinedfunctionundefinedundefined*undefinedundefinedfetchCommits(repo) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedlet url undefinedundefined=undefinedundefined`https://api.github.com/repos/undefinedundefined${repoundefinedundefined}undefinedundefined/commits`undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedwhile (url) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedconst response undefinedundefined=undefinedundefinedawaitundefinedundefinedfetch(urlundefinedundefined,undefinedundefined{undefinedundefined// (1)undefinedundefinedundefinedundefinedundefinedundefinedheadersundefinedundefined:undefinedundefined{undefinedundefined'User-Agent'undefinedundefined:undefinedundefined'Our script'undefinedundefined},undefinedundefined// github needs any user-agent headerundefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedconst body undefinedundefined=undefinedundefinedawaitundefinedundefinedresponse.undefinedundefinedjson()undefinedundefined;undefinedundefined// (2) response is JSON (array of commits)undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// (3) the URL of the next page is in the headers, extract itundefinedundefinedundefinedundefinedundefinedundefinedlet nextPage undefinedundefined=undefinedundefinedresponse.undefinedundefinedheaders.undefinedundefinedget(undefinedundefined'Link').undefinedundefinedmatch(undefinedundefined/<undefinedundefined(undefinedundefined.undefinedundefined*?)undefinedundefined>; rel="next"/)undefinedundefined;undefinedundefinedundefinedundefined nextPage undefinedundefined= nextPageundefinedundefined?.[undefinedundefined1]undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined url undefinedundefined= nextPageundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfor(undefinedundefinedlet commit undefinedundefinedof body) undefinedundefined{undefinedundefined// (4) yield commits one by one, until the page endsundefinedundefinedundefinedundefinedundefinedundefinedyield commitundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefined }undefinedundefined}undefinedundefinedundefinedundefined
undefinedundefined
More explanations about how it works:
undefinedundefinedWe use the browser undefinedundefinedfetch method to download the commits.undefinedundefined
undefinedundefinedhttps://api.github.com/repos/<repo>/commits, and the next page will be in
the undefinedundefinedLink header of the response.undefinedundefinedfetch method allows us to supply authorization and other headers if needed - here
GitHub requires undefinedundefinedUser-Agent.undefinedundefinedLink header of the response. It has a special
format, so we use a regular expression for that (we will learn this feature in undefinedundefinedRegular expressions).
undefinedundefinedhttps://api.github.com/repositories/93253246/commits?page=2. It's generated by
GitHub itself.undefinedundefinedThen we yield the received commits one by one, and when they finish, the next
undefinedundefinedwhile(url) iteration will trigger, making one more request.undefinedundefined
An example of use (shows commit authors in console):
undefinedundefinedrun (async () => {
undefinedundefinedlet count = 0;
undefinedundefinedfor await (const commit of fetchCommits(‘javascript-tutorial/en.javascript.info')) {
undefinedundefinedundefinedundefinedconsole.log(commit.author.login);
if (++count == 100) { // let's stop at 100 commits
break;
}undefinedundefinedundefinedundefined}
undefinedundefined})();
undefinedundefined// Note: If you are running this in an external sandbox, you'll need to paste here the function fetchCommits described above
undefinedundefinedThat's just what we wanted.
undefinedundefinedThe internal mechanics of paginated requests is invisible from the outside. For us it's just an async generator that returns commits.
undefinedundefinedRegular iterators and generators work fine with the data that doesn't take time to generate.
undefinedundefinedWhen we expect the data to come asynchronously, with
delays, their async counterparts can be used, and undefinedundefinedfor await..of instead of
undefinedundefinedfor..of.undefinedundefined
Syntax differences between async and regular iterators:
undefinedundefined| undefinedundefined | Iterable | undefinedundefinedAsync Iterable | undefinedundefined
|---|---|---|
| Method to provide iterator | undefinedundefinedundefinedundefinedSymbol.iteratorundefinedundefined | undefinedundefined
undefinedundefinedSymbol.asyncIteratorundefinedundefined | undefinedundefined
undefinedundefinednext() return value
isundefinedundefined | undefinedundefined
undefinedundefined{value:…, done: true/false}undefinedundefined | undefinedundefined
undefinedundefinedPromise that resolves to
undefinedundefined{value:…, done: true/false}undefinedundefined | undefinedundefined
Syntax differences between async and regular generators:
undefinedundefined| undefinedundefined | Generators | undefinedundefinedAsync generators | undefinedundefined
|---|---|---|
| Declaration | undefinedundefinedundefinedundefinedfunction*undefinedundefined | undefinedundefined
undefinedundefinedasync function*undefinedundefined | undefinedundefined
undefinedundefinednext() return value isundefinedundefined |
undefinedundefinedundefinedundefined{value:…, done: true/false}undefinedundefined |
undefinedundefinedundefinedundefinedPromise that resolves to
undefinedundefined{value:…, done: true/false}undefinedundefined | undefinedundefined
In web-development we often meet streams of data, when it flows chunk-by-chunk. For instance, downloading or uploading a big file.
undefinedundefinedWe can use async generators to process such data. It's also noteworthy that in some environments, like in browsers, there's also another API called Streams, that provides special interfaces to work with such streams, to transform the data and to pass it from one stream to another (e.g. download from one place and immediately send elsewhere).
undefinedundefinedAs our application grows bigger, we want to split it into multiple files, so called "modules". A module may contain a class or a library of functions for a specific purpose.
undefinedundefinedFor a long time, JavaScript existed without a language-level module syntax. That wasn't a problem, because initially scripts were small and simple, so there was no need.
undefinedundefinedBut eventually scripts became more and more complex, so the community invented a variety of ways to organize code into modules, special libraries to load modules on demand.
undefinedundefinedTo name some (for historical reasons):
undefinedundefinedNow all these slowly become a part of history, but we still can find them in old scripts.
undefinedundefinedThe language-level module system appeared in the standard in 2015, gradually evolved since then, and is now supported by all major browsers and in Node.js. So we'll study the modern JavaScript modules from now on.
undefinedundefinedA module is just a file. One script is one module. As simple as that.
undefinedundefinedModules can load each
other and use special directives undefinedundefinedexport and undefinedundefinedimport to
interchange functionality, call functions of one module from another one:undefinedundefined
export keyword labels variables and functions that should be
accessible from outside the current module.undefinedundefinedimport allows the import of functionality from other modules.undefinedundefinedFor instance, if we have a file undefinedundefinedsayHi.js
exporting a function:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 sayHi.jsundefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefinedfunctionundefinedundefinedsayHi(user) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`Hello, undefinedundefined${userundefinedundefined}undefinedundefined!`)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…Then another file may import and use it:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// 📁 main.jsundefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined{sayHiundefinedundefined}undefinedundefinedfromundefinedundefined'./sayHi.js'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedalert(sayHi)undefinedundefined;undefinedundefined// function...undefinedundefinedundefinedundefinedundefinedundefinedsayHi(undefinedundefined'John')undefinedundefined;undefinedundefined// Hello, John!undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The undefinedundefinedimport directive loads the module by path
undefinedundefined./sayHi.js relative to the current file, and assigns exported function
undefinedundefinedsayHi to the corresponding variable.undefinedundefined
Let's run the example in-browser.
undefinedundefinedAs modules support special keywords and features, we must tell the
browser that a script should be treated as a module, by using the attribute
undefinedundefined<script type="module">.undefinedundefined
Like this:
undefinedundefined[codetabs src="say" height="140" current="index.html"]
undefinedundefinedThe browser automatically fetches and evaluates the imported module (and its imports if needed), and then runs the script.
undefinedundefined
undefinedundefinedwarn header="Modules work only via HTTP(s), not in local files" If you try to open a web-page locally, via `file://` protocol, you'll find that `import/export` directives don't work. Use a local web-server, such as [static-server](https://www.npmjs.com/package/static-server#getting-started) or use the "live server" capability of your editor, such as VS Code [Live Server Extension](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer) to test modules.undefinedundefined
What's different in modules, compared to "regular" scripts?
undefinedundefinedThere are core features, valid both for browser and server-side JavaScript.
undefinedundefined
Modules always undefinedundefineduse strict, by default. E.g. assigning to an undeclared variable will
give an error.undefinedundefined
undefinedundefinedhtml run <script type="module"> a = 5; // error </script>undefinedundefined
Each module has its own top-level scope. In other words, top-level variables and functions from a module are not seen in other scripts.
undefinedundefinedIn the example below, two scripts are imported, and undefinedundefinedhello.js tries
to use undefinedundefineduser variable declared in undefinedundefineduser.js, and
fails:undefinedundefined
[codetabs src="scopes" height="140" current="index.html"]
undefinedundefinedModules are expected to undefinedundefinedexport what they want to be accessible from
outside and undefinedundefinedimport what they need.undefinedundefined
So we
should import undefinedundefineduser.js into undefinedundefinedhello.js and get the required
functionality from it instead of relying on global variables.undefinedundefined
This is the correct variant:
undefinedundefined[codetabs src="scopes-working" height="140" current="hello.js"]
undefinedundefinedIn the browser, independent top-level scope also exists for each
undefinedundefined<script type="module">:undefinedundefined
undefinedundefined
If we really need to make a window-level global variable, we can explicitly assign it to
undefinedundefinedwindow and access as undefinedundefinedwindow.user. But that's an
exception requiring a good reason.undefinedundefined
If the same module is imported into multiple other places, its code is executed only the first time, then exports are given to all importers.
undefinedundefinedThat has important consequences. Let's look at them using examples:
undefinedundefinedFirst, if executing a module code brings side-effects, like showing a message, then importing it multiple times will trigger it only once - the first time:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// 📁 alert.jsundefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined"Module is evaluated!")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// Import the same module from different filesundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// 📁 1.jsundefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined`./alert.js`undefinedundefined;undefinedundefined// Module is evaluated!undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// 📁 2.jsundefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined`./alert.js`undefinedundefined;undefinedundefined// (shows nothing)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In practice, top-level module code is mostly used for initialization, creation of internal data structures, and if we want something to be reusable - export it.
undefinedundefinedNow, a more advanced example.
undefinedundefinedLet's say, a module exports an object:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// 📁 admin.jsundefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefinedlet admin undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinednameundefinedundefined:undefinedundefined"John"undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
If this module is imported from multiple files, the module is only evaluated the first time,
undefinedundefinedadmin object is created, and then passed to all further importers.undefinedundefined
All importers get exactly the one and only undefinedundefinedadmin
object:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 1.jsundefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined{adminundefinedundefined}undefinedundefinedfromundefinedundefined'./admin.js'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedadmin.undefinedundefinednameundefinedundefined=undefinedundefined"Pete"undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// 📁 2.jsundefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined{adminundefinedundefined}undefinedundefinedfromundefinedundefined'./admin.js'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefinedadmin.undefinedundefinedname)undefinedundefined;undefinedundefined// Peteundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefined// Both 1.js and 2.js imported the same objectundefinedundefinedundefinedundefinedundefinedundefined// Changes made in 1.js are visible in 2.jsundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
So, let's reiterate - the module is executed only once. Exports are generated, and then they
are shared between importers, so if something changes the undefinedundefinedadmin object, other modules
will see that.undefinedundefined
Such behavior allows us to undefinedundefinedconfigure modules on first import. We can setup its properties once, and then in further imports it's ready.undefinedundefined
undefinedundefinedFor instance, the
undefinedundefinedadmin.js module may provide certain functionality, but expect the credentials to come
into the undefinedundefinedadmin object from outside:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 admin.jsundefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefinedlet admin undefinedundefined=undefinedundefined{undefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefinedfunctionundefinedundefinedsayHi() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`Ready to serve, undefinedundefined${undefinedundefinedadmin.undefinedundefinednameundefinedundefined}undefinedundefined!`)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In undefinedundefinedinit.js, the first script of our app, we set
undefinedundefinedadmin.name. Then everyone will see it, including calls made from inside
undefinedundefinedadmin.js itself:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 init.jsundefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined{adminundefinedundefined}undefinedundefinedfromundefinedundefined'./admin.js'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedadmin.undefinedundefinednameundefinedundefined=undefinedundefined"Pete"undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Another module can also see undefinedundefinedadmin.name:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 other.jsundefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined{adminundefinedundefined, sayHiundefinedundefined}undefinedundefinedfromundefinedundefined'./admin.js'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefinedadmin.undefinedundefinedname)undefinedundefined;undefinedundefined// *!*Pete*/!*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedsayHi()undefinedundefined;undefinedundefined// Ready to serve, *!*Pete*/!*!undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The object
undefinedundefinedimport.meta contains the information about the current module.undefinedundefined
Its content depends on the environment. In the browser, it contains the url of the script, or a current webpage url if inside HTML:
undefinedundefined
undefinedundefinedhtml run height=0 <script type="module"> alert(import.meta.url); // script url (url of the html page for an inline script) </script>undefinedundefined
That's kind of a minor feature, but for completeness we should mention it.
undefinedundefinedIn a module,
top-level undefinedundefinedthis is undefined.undefinedundefined
Compare it to
non-module scripts, where undefinedundefinedthis is a global object:undefinedundefined
undefinedundefined
There are
also several browser-specific differences of scripts with undefinedundefinedtype="module" compared to
regular ones.undefinedundefined
You may want skip this section for now if you're reading for the first time, or if you don't use JavaScript in a browser.
undefinedundefinedModule scripts are
undefinedundefinedalways deferred, same effect as undefinedundefineddefer attribute (described
in the chapter undefinedundefined), for both external and inline
scripts.undefinedundefined
In other words: - downloading external module scripts
undefinedundefined<script type="module" src="..."> doesn't block HTML processing, they load in
parallel with other resources. - module scripts wait until the HTML document is fully ready (even if they are tiny and
load faster than HTML), and then run. - relative order of scripts is maintained: scripts that go first in the
document, execute first.undefinedundefined
As a side-effect, module scripts always "see" the fully loaded HTML-page, including HTML elements below them.
undefinedundefinedFor instance:
run undefinedundefined undefinedundefinedCompare to regular script below:
undefinedundefined undefinedundefinedundefinedundefinedundefinedundefined
Please note: the second script actually runs before the first! So we'll see
undefinedundefinedundefined first, and then undefinedundefinedobject.undefinedundefined
That's because modules are deferred, so we wait for the document to be processed. The regular script runs immediately, so we see its output first.
undefinedundefinedWhen using modules, we should be aware that the HTML page shows up as it loads, and JavaScript modules run after that, so the user may see the page before the JavaScript application is ready. Some functionality may not work yet. We should put "loading indicators", or otherwise ensure that the visitor won't be confused by that.
undefinedundefinedFor non-module scripts, the
undefinedundefinedasync attribute only works on external scripts. Async scripts run immediately when
ready, independently of other scripts or the HTML document.undefinedundefined
For module scripts, it works on inline scripts as well.
undefinedundefinedFor example, the inline script below has
undefinedundefinedasync, so it doesn't wait for anything.undefinedundefined
It
performs the import (fetches undefinedundefined./analytics.js) and runs when ready, even if the HTML
document is not finished yet, or if other scripts are still pending.undefinedundefined
That's good for functionality that doesn't depend on anything, like counters, ads, document-level event listeners.
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined<!-- all dependencies are fetched (analytics.js), and the script runs -->undefinedundefinedundefinedundefinedundefinedundefined<!-- doesn't wait for the document or other <script> tags -->undefinedundefinedundefinedundefinedundefinedundefined<scriptundefinedundefined *undefinedundefined!*async*/!*undefinedundefined type=undefinedundefined"module"undefinedundefined>undefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined{counterundefinedundefined}undefinedundefinedfromundefinedundefined'./analytics.js'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedcounter.undefinedundefinedcount()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined</script>undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
External scripts that have
undefinedundefinedtype="module" are different in two aspects:undefinedundefined
External scripts with the same undefinedundefinedsrc
run only once:
undefinedundefinedhtml <!-- the script my.js is fetched and executed only once --> <script type="module" src="my.js"></script> <script type="module" src="my.js"></script>undefinedundefined
External scripts that are fetched from another
origin (e.g. another site) require undefinedundefinedCORS headers, as described in
the chapter undefinedundefinedinfo:fetch-crossorigin. In other
words, if a module script is fetched from another origin, the remote server must supply a header
undefinedundefinedAccess-Control-Allow-Origin allowing the fetch.
undefinedundefinedhtml <!-- another-site.com must supply Access-Control-Allow-Origin --> <!-- otherwise, the script won't execute --> <script type="module" src="*!*http://another-site.com/their.js*/!*"></script>undefinedundefined
That ensures better security by default.
undefinedundefinedIn the browser,
undefinedundefinedimport must get either a relative or absolute URL. Modules without any path are called
"bare" modules. Such modules are not allowed in undefinedundefinedimport.undefinedundefined
For instance, this undefinedundefinedimport is invalid:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined{sayHiundefinedundefined}undefinedundefinedfromundefinedundefined'sayHi'undefinedundefined;undefinedundefined// Error, "bare" moduleundefinedundefinedundefinedundefinedundefinedundefined// the module must have a path, e.g. './sayHi.js' or wherever the module isundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Certain environments, like Node.js or bundle tools allow bare modules, without any path, as they have their own ways for finding modules and hooks to fine-tune them. But browsers do not support bare modules yet.
undefinedundefinedOld
browsers do not understand undefinedundefinedtype="module". Scripts of an unknown type are just ignored.
For them, it's possible to provide a fallback using the undefinedundefinednomodule
attribute:undefinedundefined
undefinedundefined
In real-life, browser modules are rarely used in their "raw" form. Usually, we bundle them together with a special tool such as undefinedundefinedWebpack and deploy to the production server.undefinedundefined
undefinedundefinedOne of the benefits of using bundlers - they give more control over how modules are resolved, allowing bare modules and much more, like CSS/HTML modules.
undefinedundefinedBuild tools do the following:
undefinedundefined<script type="module"> in HTML.undefinedundefinedimport calls
with bundler functions, so that it works. "Special" module types like HTML/CSS modules are also
supported.undefinedundefinedconsole and undefinedundefineddebugger removed.undefinedundefined
If we use bundle
tools, then as scripts are bundled together into a single file (or few files),
undefinedundefinedimport/export statements inside those scripts are replaced by special bundler
functions. So the resulting "bundled" script does not contain any undefinedundefinedimport/export, it
doesn't require undefinedundefinedtype="module", and we can put it into a regular
script:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined<!-- Assuming we got bundle.js from a tool like Webpack -->undefinedundefinedundefinedundefinedundefinedundefined<scriptundefinedundefined src=undefinedundefined"bundle.js"undefinedundefined></script>undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That said, native modules are also usable. So we won't be using Webpack here: you can configure it later.
undefinedundefinedTo summarize, the core concepts are:
undefinedundefinedimport/export work, browsers need
undefinedundefined<script type="module">. Modules have several differences:
undefinedundefinedimport/export.undefinedundefineduse strict.undefinedundefinedWhen we use modules, each module implements the functionality and exports it. Then we use
undefinedundefinedimport to directly import it where it's needed. The browser loads and evaluates the
scripts automatically.undefinedundefined
In production, people often use bundlers such as undefinedundefinedWebpack to bundle modules together for performance and other reasons.undefinedundefined
undefinedundefinedIn the next chapter we'll see more examples of modules, and how things can be exported/imported.
undefinedundefinedExport and import directives have several syntax variants.
undefinedundefinedIn the previous article we saw a simple use, now let's explore more examples.
undefinedundefinedWe can label any declaration as
exported by placing undefinedundefinedexport before it, be it a variable, function or a
class.undefinedundefined
For instance, here all exports are valid:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// export an arrayundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedexportundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefined let months = undefinedundefined['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// export a constantundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined!undefinedundefined*undefinedundefinedexportundefinedundefined*undefinedundefined/undefinedundefined!*undefinedundefinedconst MODULES_BECAME_STANDARD_YEAR undefinedundefined=undefinedundefined2015undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// export a classundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedexportundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefined class User {undefinedundefinedundefinedundefinedundefinedundefined constructorundefinedundefined(undefinedundefinednameundefinedundefined)undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined this.name = name;undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedsmart header="No semicolons after export class/function" Please note thatexport` before
a class or a function does not make it a undefinedundefinedfunction
expression. It's still a function declaration, albeit exported.undefinedundefined
Most JavaScript style guides don't recommend semicolons after function and class declarations.
undefinedundefined
That's why there's no need for a semicolon at the end of undefinedundefinedexport class and
undefinedundefinedexport function:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefinedfunctionundefinedundefinedsayHi(user) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`Hello, undefinedundefined${userundefinedundefined}undefinedundefined!`)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefined*!*undefinedundefined// no ; at the end */!*undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefined
Also, we can put undefinedundefinedexport separately.undefinedundefined
Here we first declare, and then export:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// 📁 say.jsundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedsayHi(user) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`Hello, undefinedundefined${userundefinedundefined}undefinedundefined!`)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedsayBye(user) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`Bye, undefinedundefined${userundefinedundefined}undefinedundefined!`)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefined{sayHiundefinedundefined, sayByeundefinedundefined};undefinedundefined// a list of exported variablesundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…Or, technically we could put undefinedundefinedexport above functions as
well.undefinedundefined
Usually, we put a list
of what to import in curly braces undefinedundefinedimport {...}, like this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 main.jsundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined{sayHiundefinedundefined, sayByeundefinedundefined}undefinedundefinedfromundefinedundefined'./say.js'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedsayHiundefinedundefined(undefinedundefined'John'undefinedundefined)undefinedundefined; // Hello, John!undefinedundefinedundefinedundefinedundefinedundefinedsayByeundefinedundefined(undefinedundefined'John'undefinedundefined)undefinedundefined; // Bye, John!undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
But if there's a lot to import, we can import everything as an object using
undefinedundefinedimport * as <obj>, for instance:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 main.jsundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined*undefinedundefinedas say undefinedundefinedfromundefinedundefined'./say.js'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedsay.sayHiundefinedundefined(undefinedundefined'John'undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedsay.sayByeundefinedundefined(undefinedundefined'John'undefinedundefined)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
At first sight, "import everything" seems such a cool thing, short to write, why should we ever explicitly list what we need to import?
undefinedundefinedWell, there are few reasons.
undefinedundefinedModern build tools (undefinedundefinedwebpack and others) bundle modules together and optimize them to speedup loading and remove unused stuff.undefinedundefined
undefinedundefinedLet's say, we added a 3rd-party library
undefinedundefinedsay.js to our project with many functions:
undefinedundefinedjs // 📁 say.js export function sayHi() { ... } export function sayBye() { ... } export function becomeSilent() { ... }undefinedundefined
Now if we only use one of undefinedundefinedsay.js functions in our project:
undefinedundefinedjs // 📁 main.js import {sayHi} from './say.js'; …Then the optimizer will see
that and remove the other functions from the bundled code, thus making the build smaller. That is called
"tree-shaking".undefinedundefined
sayHi() instead of
undefinedundefinedsay.sayHi().undefinedundefinedExplicit list of imports gives better overview of the code structure: what is used and where. It makes code support and refactoring easier.
undefinedundefinedWe can also use undefinedundefinedas to import under
different names.undefinedundefined
For instance, let's import
undefinedundefinedsayHi into the local variable undefinedundefinedhi for brevity, and import
undefinedundefinedsayBye as undefinedundefinedbye:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 main.jsundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined{sayHi undefinedundefinedas hiundefinedundefined, sayBye undefinedundefinedas byeundefinedundefined}undefinedundefinedfromundefinedundefined'./say.js'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedhiundefinedundefined(undefinedundefined'John'undefinedundefined)undefinedundefined; // Hello, John!undefinedundefinedundefinedundefinedundefinedundefinedbyeundefinedundefined(undefinedundefined'John'undefinedundefined)undefinedundefined; // Bye, John!undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The similar syntax exists for
undefinedundefinedexport.undefinedundefined
Let's export functions as
undefinedundefinedhi and undefinedundefinedbye:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 say.jsundefinedundefinedundefinedundefined...undefinedundefinedundefinedundefinedexportundefinedundefined{sayHi undefinedundefinedas hiundefinedundefined, sayBye undefinedundefinedas byeundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Now undefinedundefinedhi and undefinedundefinedbye are official
names for outsiders, to be used in imports:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 main.jsundefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined*undefinedundefinedas say undefinedundefinedfromundefinedundefined'./say.js'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedsay.undefinedundefined*!*hiundefinedundefined*undefinedundefined/!undefinedundefined*(undefinedundefined'John'undefinedundefined)undefinedundefined; // Hello, John!undefinedundefinedundefinedundefinedundefinedundefinedsay.undefinedundefined*undefinedundefined!undefinedundefined*undefinedundefinedbyeundefinedundefined*undefinedundefined/undefinedundefined!*(undefinedundefined'John')undefinedundefined;undefinedundefined// Bye, John!undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In practice, there are mainly two kinds of modules.
undefinedundefinedsay.js above.undefinedundefineduser.js exports only
undefinedundefinedclass User.undefinedundefinedMostly, the second approach is preferred, so that every "thing" resides in its own module.
undefinedundefinedNaturally, that requires a lot of files, as everything wants its own module, but that's not a problem at all. Actually, code navigation becomes easier if files are well-named and structured into folders.
undefinedundefinedModules provide
a special undefinedundefinedexport default ("the default export") syntax to make the "one thing per
module" way look better.undefinedundefined
Put undefinedundefinedexport default
before the entity to export:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 user.jsundefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefined*!*undefinedundefineddefaultundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefined class User { // just add "default"undefinedundefinedundefinedundefinedundefinedundefined constructorundefinedundefined(undefinedundefinednameundefinedundefined)undefinedundefined {undefinedundefinedundefinedundefinedundefinedundefined this.name = name;undefinedundefinedundefinedundefinedundefinedundefined }undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
There may be only one undefinedundefinedexport default per
file.undefinedundefined
…And then import it without curly braces:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// 📁 main.jsundefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined*!*Userundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefined from './user.undefinedundefinedjsundefinedundefined'; // not {User}, just UserundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinednewundefinedundefinedUser(undefinedundefined'John')undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Imports without curly braces look nicer. A common mistake when starting to use modules is to
forget curly braces at all. So, remember, undefinedundefinedimport needs curly braces for named exports
and doesn't need them for the default one.undefinedundefined
| Named export | undefinedundefinedDefault export | undefinedundefined
|---|---|
undefinedundefinedexport class User {...}undefinedundefined |
undefinedundefinedundefinedundefinedexport default class User {...}undefinedundefined |
undefinedundefined
undefinedundefinedimport {User} from ...undefinedundefined | undefinedundefined
undefinedundefinedimport User from ...undefinedundefined | undefinedundefined
Technically, we may have both default and named exports in a single module, but in practice people usually don't mix them. A module has either named exports or the default one.
undefinedundefinedAs there may be at most one default export per file, the exported entity may have no name.
undefinedundefinedFor instance, these are all perfectly valid default exports:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefineddefaultundefinedundefinedclassundefinedundefined{undefinedundefined// no class nameundefinedundefinedundefinedundefinedundefinedundefinedconstructor() undefinedundefined{ ... undefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefineddefaultundefinedundefinedfunction(user) undefinedundefined{undefinedundefined// no function nameundefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`Hello, undefinedundefined${userundefinedundefined}undefinedundefined!`)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// export a single value, without making a variableundefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefineddefault [undefinedundefined'Jan'undefinedundefined,undefinedundefined'Feb'undefinedundefined,undefinedundefined'Mar'undefinedundefined,undefinedundefined'Apr'undefinedundefined,undefinedundefined'Aug'undefinedundefined,undefinedundefined'Sep'undefinedundefined,undefinedundefined'Oct'undefinedundefined,undefinedundefined'Nov'undefinedundefined,undefinedundefined'Dec']undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Not giving a name is fine, because there is only one
undefinedundefinedexport default per file, so undefinedundefinedimport without curly braces
knows what to import.undefinedundefined
Without undefinedundefineddefault, such
an export would give an error:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefinedclassundefinedundefined{undefinedundefined// Error! (non-default export needs a name)undefinedundefinedundefinedundefinedundefinedundefinedconstructor() undefinedundefined{}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In some situations the
undefinedundefineddefault keyword is used to reference the default export.undefinedundefined
For example, to export a function separately from its definition:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedsayHi(user) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`Hello, undefinedundefined${userundefinedundefined}undefinedundefined!`)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// same as if we added "export default" before the functionundefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefined{sayHi undefinedundefinedasundefinedundefineddefaultundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Or, another situation, let's say a module undefinedundefineduser.js exports one
main "default" thing, and a few named ones (rarely the case, but it happens):undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 user.jsundefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefineddefaultundefinedundefinedclass User undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedconstructor(name) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedthis.undefinedundefinednameundefinedundefined= nameundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefinedfunctionundefinedundefinedsayHi(user) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`Hello, undefinedundefined${userundefinedundefined}undefinedundefined!`)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Here's how to import the default export along with a named one:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// 📁 main.jsundefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined{*!*undefinedundefineddefaultundefinedundefinedas Userundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefined, sayHi} from './user.undefinedundefinedjsundefinedundefined';undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinednewundefinedundefinedUser(undefinedundefined'John')undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
And, finally, if importing everything undefinedundefined* as an object, then the
undefinedundefineddefault property is exactly the default export:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 main.jsundefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined*undefinedundefinedas user undefinedundefinedfromundefinedundefined'./user.js'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet User undefinedundefined=undefinedundefineduser.undefinedundefineddefaultundefinedundefined;undefinedundefined// the default exportundefinedundefinedundefinedundefinedundefinedundefinednewundefinedundefinedUser(undefinedundefined'John')undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Named exports are explicit. They exactly name what they import, so we have that information from them; that's a good thing.
undefinedundefinedNamed exports force us to use exactly the right name to import:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined{Userundefinedundefined}undefinedundefinedfromundefinedundefined'./user.js'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined// import {MyUser} won't work, the name must be {User}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…While for a default export, we always choose the name when importing:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedimport User undefinedundefinedfromundefinedundefined'./user.js'undefinedundefined;undefinedundefined// worksundefinedundefinedundefinedundefinedundefinedundefinedimport MyUser undefinedundefinedfromundefinedundefined'./user.js'undefinedundefined;undefinedundefined// works tooundefinedundefinedundefinedundefinedundefinedundefined// could be import Anything... and it'll still workundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
So team members may use different names to import the same thing, and that's not good.
undefinedundefinedUsually, to avoid that and keep the code consistent, there's a rule that imported variables should correspond to file names, e.g:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedimport User undefinedundefinedfromundefinedundefined'./user.js'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedimport LoginForm undefinedundefinedfromundefinedundefined'./loginForm.js'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedimport func undefinedundefinedfromundefinedundefined'/path/to/func.js'undefinedundefined;undefinedundefinedundefinedundefined...undefinedundefinedundefinedundefined
undefinedundefined
Still, some teams consider it a serious drawback of default exports. So they prefer to always
use named exports. Even if only a single thing is exported, it's still exported under a name, without
undefinedundefineddefault.undefinedundefined
That also makes re-export (see below) a little bit easier.
undefinedundefined"Re-export"
syntax undefinedundefinedexport ... from ... allows to import things and immediately export them
(possibly under another name), like this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefined{sayHiundefinedundefined}undefinedundefinedfromundefinedundefined'./say.js'undefinedundefined;undefinedundefined// re-export sayHiundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefined{undefinedundefineddefaultundefinedundefinedas Userundefinedundefined}undefinedundefinedfromundefinedundefined'./user.js'undefinedundefined;undefinedundefined// re-export defaultundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Why would that be needed? Let's see a practical use case.
undefinedundefinedImagine, we're writing a "package": a folder with a lot of modules, with some of the functionality exported outside (tools like NPM allow us to publish and distribute such packages, but we don't have to use them), and many modules are just "helpers", for internal use in other package modules.
undefinedundefinedThe file structure could be like this:
undefinedundefinedundefinedundefinedauth/
index.js
user.js
helpers.js
tests/
login.js
providers/
github.js
facebook.js
...undefinedundefinedundefinedundefinedWe'd like to expose the package functionality via a single entry point.
undefinedundefinedIn other words, a person who would like to use our package, should import only from the
"main file" undefinedundefinedauth/index.js.undefinedundefined
Like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined{loginundefinedundefined, logoutundefinedundefined}undefinedundefinedfromundefinedundefined'auth/index.js'undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The "main file", undefinedundefinedauth/index.js exports all the functionality
that we'd like to provide in our package.undefinedundefined
The idea is that outsiders, other
programmers who use our package, should not meddle with its internal structure, search for files inside our package
folder. We export only what's necessary in undefinedundefinedauth/index.js and keep the rest hidden from
prying eyes.undefinedundefined
As the actual exported functionality is scattered among the
package, we can import it into undefinedundefinedauth/index.js and export from it:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 auth/index.jsundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// import login/logout and immediately export themundefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined{loginundefinedundefined, logoutundefinedundefined}undefinedundefinedfromundefinedundefined'./helpers.js'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefined{loginundefinedundefined, logoutundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// import default as User and export itundefinedundefinedundefinedundefinedundefinedundefinedimport User undefinedundefinedfromundefinedundefined'./user.js'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefined{Userundefinedundefined};undefinedundefinedundefinedundefined...undefinedundefinedundefinedundefined
undefinedundefined
Now users of our package can
undefinedundefinedimport {login} from "auth/index.js".undefinedundefined
The
syntax undefinedundefinedexport ... from ... is just a shorter notation for such
import-export:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 auth/index.jsundefinedundefinedundefinedundefinedundefinedundefined// re-export login/logout undefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefined{loginundefinedundefined, logoutundefinedundefined}undefinedundefinedfromundefinedundefined'./helpers.js'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// re-export the default export as Userundefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefined{undefinedundefineddefaultundefinedundefinedas Userundefinedundefined}undefinedundefinedfromundefinedundefined'./user.js'undefinedundefined;undefinedundefinedundefinedundefined...undefinedundefinedundefinedundefined
undefinedundefined
The notable difference of undefinedundefinedexport ... from compared to
undefinedundefinedimport/export is that re-exported modules aren't available in the current file. So
inside the above example of undefinedundefinedauth/index.js we can't use re-exported
undefinedundefinedlogin/logout functions.undefinedundefined
The default export needs separate handling when re-exporting.
undefinedundefinedLet's say we have undefinedundefineduser.js
with the undefinedundefinedexport default class User and would like to re-export it:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 user.jsundefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefineddefaultundefinedundefinedclass User undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
We can come across two problems with it:
undefinedundefinedundefinedundefinedexport User from './user.js' won't work. That would lead to a
syntax error.undefinedundefined
To re-export the default export, we have to write
undefinedundefinedexport {default as User}, as in the example above.undefinedundefined
undefinedundefinedexport * from './user.js' re-exports only named exports, but ignores the default
one.undefinedundefined
If we'd like to re-export both named and the default export, then
two statements are needed:
undefinedundefinedjs export * from './user.js'; // to re-export named exports export {default} from './user.js'; // to re-export the default exportundefinedundefined
Such oddities of re-exporting a default export are one of the reasons why some developers don't like default exports and prefer named ones.
undefinedundefinedHere are all types of
undefinedundefinedexport that we covered in this and previous articles.undefinedundefined
You can check yourself by reading them and recalling what they mean:
undefinedundefinedexport [default] class/function/variable ...undefinedundefinedexport {x [as y], ...}.undefinedundefined
export {x [as y], ...} from "module"undefinedundefinedexport * from "module" (doesn't re-export default).undefinedundefinedexport {default [as y]} from "module" (re-export
default).undefinedundefinedImport:
undefinedundefinedimport {x [as y], ...} from "module"undefinedundefinedimport x from "module"undefinedundefined
import {default as x} from "module"undefinedundefined
import * as obj from "module"undefinedundefinedimport "module"undefinedundefinedWe can put
undefinedundefinedimport/export statements at the top or at the bottom of a script, that doesn't
matter.undefinedundefined
So, technically this code is fine:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedsayHi()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined{sayHiundefinedundefined}undefinedundefinedfromundefinedundefined'./say.js'undefinedundefined;undefinedundefined// import at the end of the fileundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In practice imports are usually at the start of the file, but that's only for more convenience.
undefinedundefinedundefinedundefinedPlease note that import/export statements don't work if
inside undefinedundefined{...}.undefinedundefinedundefinedundefined
A conditional import, like this, won't work:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedif (something) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedimportundefinedundefined{sayHiundefinedundefined}undefinedundefinedfromundefinedundefined"./say.js"undefinedundefined;undefinedundefined// Error: import must be at top levelundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…But what if we really need to import something conditionally? Or at the right time? Like, load a module upon request, when it's really needed?
undefinedundefinedWe'll see dynamic imports in the next article.
undefinedundefinedExport and import statements that we covered in previous chapters are called "static". The syntax is very simple and strict.
undefinedundefinedFirst, we can't dynamically generate any parameters of
undefinedundefinedimport.undefinedundefined
The module path must be a primitive string, can't be a function call. This won't work:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedimport ... undefinedundefinedfromundefinedundefined*!*undefinedundefinedgetModuleName()undefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefined; // Error, only from "string" is allowedundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Second, we can't import conditionally or at run-time:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedif(...) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedimport ...undefinedundefined;undefinedundefined// Error, not allowed!undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedimport ...undefinedundefined;undefinedundefined// Error, we can't put import in any blockundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
That's because undefinedundefinedimport/undefinedundefinedexport
aim to provide a backbone for the code structure. That's a good thing, as code structure can be analyzed, modules can
be gathered and bundled into one file by special tools, unused exports can be removed ("tree-shaken"). That's possible
only because the structure of imports/exports is simple and fixed.undefinedundefined
But how can we import a module dynamically, on-demand?
undefinedundefinedThe undefinedundefinedimport(module) expression loads the module and
returns a promise that resolves into a module object that contains all its exports. It can be called from any place in
the code.undefinedundefined
We can use it dynamically in any place of the code, for instance:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet modulePath undefinedundefined=undefinedundefinedprompt(undefinedundefined"Which module to load?")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedimport(modulePath)undefinedundefinedundefinedundefined .undefinedundefinedthen(obj undefinedundefined=>undefinedundefined<module objectundefinedundefined>)undefinedundefinedundefinedundefined .undefinedundefinedcatch(err undefinedundefined=>undefinedundefined<loading errorundefinedundefined,undefinedundefinede.undefinedundefinedg. undefinedundefinedif no such moduleundefinedundefined>)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Or, we could use undefinedundefinedlet module = await import(modulePath) if
inside an async function.undefinedundefined
For instance, if we have the following module
undefinedundefinedsay.js:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 say.jsundefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefinedfunctionundefinedundefinedhi() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`Hello`)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefinedfunctionundefinedundefinedbye() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`Bye`)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…Then dynamic import can be like this:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedletundefinedundefined{hiundefinedundefined, byeundefinedundefined}undefinedundefined=undefinedundefinedawaitundefinedundefinedimport(undefinedundefined'./say.js')undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedhi()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedbye()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Or, if undefinedundefinedsay.js has the default export:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// 📁 say.jsundefinedundefinedundefinedundefinedundefinedundefinedexportundefinedundefineddefaultundefinedundefinedfunction() undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined"Module loaded (export default)!")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…Then, in order to access it, we can use undefinedundefineddefault property of
the module object:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet obj undefinedundefined=undefinedundefinedawaitundefinedundefinedimport(undefinedundefined'./say.js')undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedlet say undefinedundefined=undefinedundefinedobj.undefinedundefineddefaultundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined// or, in one line: let {default: say} = await import('./say.js');undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedsay()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Here's the full example:
undefinedundefined[codetabs src="say" current="index.html"]
undefinedundefinedundefinedundefinedDynamic imports work in regular scripts, they don't require `script type="module"`.undefinedundefined
undefinedundefined
undefinedundefinedAlthough `import()` looks like a function call, it's a special syntax that just happens to use parentheses (similar to `super()`).
So we can't copy `import` to a variable or use `call/apply` with it. It's not a function.undefinedundefined
undefinedundefinedA
undefinedundefinedProxy object wraps another object and intercepts operations, like reading/writing
properties and others, optionally handling them on its own, or transparently allowing the object to handle
them.undefinedundefined
Proxies are used in many libraries and some browser frameworks. We'll see many practical applications in this article.
undefinedundefinedThe syntax:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet proxy undefinedundefined=undefinedundefinednewundefinedundefinedProxy(targetundefinedundefined, handler)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
target - is an object to wrap, can be
anything, including functions.undefinedundefinedhandler -
proxy configuration: an object with "traps", methods that intercept operations. -
e.g. undefinedundefinedget trap for reading a property of undefinedundefinedtarget,
undefinedundefinedset trap for writing a property into undefinedundefinedtarget, and so
on.undefinedundefinedFor operations on
undefinedundefinedproxy, if there's a corresponding trap in undefinedundefinedhandler, then
it runs, and the proxy has a chance to handle it, otherwise the operation is performed on
undefinedundefinedtarget.undefinedundefined
As a starting example, let's create a proxy without any traps:
undefinedundefinedrun let target = {}; let proxy = new Proxy(target, {}); // empty handler
undefinedundefinedproxy.test = 5; // writing to proxy (1) alert(target.test); // 5, the property appeared in target!
undefinedundefinedalert(proxy.test); // 5, we can read it from proxy too (2)
undefinedundefinedfor(let key in proxy) alert(key); // test, iteration works (3)
undefinedundefinedAs there are no traps, all operations on undefinedundefinedproxy are forwarded to
undefinedundefinedtarget.undefinedundefined
proxy.test= sets the value on
undefinedundefinedtarget.undefinedundefinedproxy.test returns the value from
undefinedundefinedtarget.undefinedundefinedproxy returns values from undefinedundefinedtarget.undefinedundefined
As we can see, without any traps,
undefinedundefinedproxy is a transparent wrapper around
undefinedundefinedtarget.undefinedundefined
undefinedundefinedundefinedundefined
undefinedundefinedProxy is a special
"exotic object". It doesn't have own properties. With an empty undefinedundefinedhandler it transparently
forwards operations to undefinedundefinedtarget.undefinedundefined
To activate more capabilities, let's add traps.
undefinedundefinedWhat can we intercept with them?
undefinedundefined
For most operations on objects, there's a so-called "internal method" in the JavaScript specification that describes
how it works at the lowest level. For instance undefinedundefined[[Get]], the internal method to read a
property, undefinedundefined[[Set]], the internal method to write a property, and so on. These methods
are only used in the specification, we can't call them directly by name.undefinedundefined
Proxy traps intercept invocations of these methods. They are listed in the undefinedundefinedProxy specification and in the table below.undefinedundefined
undefinedundefinedFor every internal method, there's a trap in this table:
the name of the method that we can add to the undefinedundefinedhandler parameter of
undefinedundefinednew Proxy to intercept the operation:undefinedundefined
| Internal Method | undefinedundefinedHandler Method | undefinedundefinedTriggers when… | undefinedundefined
|---|---|---|
undefinedundefined[[Get]]undefinedundefined | undefinedundefined
undefinedundefinedgetundefinedundefined | undefinedundefinedreading a property | undefinedundefined
undefinedundefined[[Set]]undefinedundefined | undefinedundefined
undefinedundefinedsetundefinedundefined | undefinedundefinedwriting to a property | undefinedundefined
undefinedundefined[[HasProperty]]undefinedundefined | undefinedundefined
undefinedundefinedhasundefinedundefined | undefinedundefinedundefinedundefinedin
operatorundefinedundefined | undefinedundefined
undefinedundefined[[Delete]]undefinedundefined | undefinedundefined
undefinedundefineddeletePropertyundefinedundefined | undefinedundefined
undefinedundefineddelete operatorundefinedundefined | undefinedundefined
undefinedundefined[[Call]]undefinedundefined |
undefinedundefinedundefinedundefinedapplyundefinedundefined | undefinedundefinedfunction call | undefinedundefined
undefinedundefined[[Construct]]undefinedundefined | undefinedundefined
undefinedundefinedconstructundefinedundefined | undefinedundefined
undefinedundefinednew operatorundefinedundefined | undefinedundefined
undefinedundefined[[GetPrototypeOf]]undefinedundefined |
undefinedundefinedundefinedundefinedgetPrototypeOfundefinedundefined | undefinedundefinedundefinedundefinedObject.getPrototypeOfundefinedundefined | undefinedundefined
undefinedundefined[[SetPrototypeOf]]undefinedundefined | undefinedundefined
undefinedundefinedsetPrototypeOfundefinedundefined | undefinedundefinedundefinedundefinedObject.setPrototypeOfundefinedundefined | undefinedundefined
undefinedundefined[[IsExtensible]]undefinedundefined | undefinedundefined
undefinedundefinedisExtensibleundefinedundefined | undefinedundefinedundefinedundefinedObject.isExtensibleundefinedundefined | undefinedundefined
undefinedundefined[[PreventExtensions]]undefinedundefined | undefinedundefined
undefinedundefinedpreventExtensionsundefinedundefined | undefinedundefinedundefinedundefinedObject.preventExtensionsundefinedundefined | undefinedundefined
undefinedundefined[[DefineOwnProperty]]undefinedundefined | undefinedundefined
undefinedundefineddefinePropertyundefinedundefined | undefinedundefinedundefinedundefinedObject.defineProperty, undefinedundefinedObject.definePropertiesundefinedundefined | undefinedundefined
undefinedundefined[[GetOwnProperty]]undefinedundefined | undefinedundefined
undefinedundefinedgetOwnPropertyDescriptorundefinedundefined | undefinedundefined
undefinedundefinedObject.getOwnPropertyDescriptor,
undefinedundefinedfor..in,
undefinedundefinedObject.keys/values/entriesundefinedundefined | undefinedundefined
undefinedundefined[[OwnPropertyKeys]]undefinedundefined | undefinedundefined
undefinedundefinedownKeysundefinedundefined | undefinedundefinedundefinedundefinedObject.getOwnPropertyNames,
undefinedundefinedObject.getOwnPropertySymbols,
undefinedundefinedfor..in,
undefinedundefinedObject.keys/values/entriesundefinedundefined | undefinedundefined
warn header="Invariants" JavaScript enforces some invariants - conditions that must be fulfilled by internal methods and traps.
undefinedundefinedMost of them are for return values: - undefinedundefined[[Set]] must
return undefinedundefinedtrue if the value was written successfully, otherwise
undefinedundefinedfalse. - undefinedundefined[[Delete]] must return
undefinedundefinedtrue if the value was deleted successfully, otherwise
undefinedundefinedfalse. - …and so on, we'll see more in examples below.undefinedundefined
There are some other invariants, like: - undefinedundefined[[GetPrototypeOf]], applied
to the proxy object must return the same value as undefinedundefined[[GetPrototypeOf]] applied to the
proxy object's target object. In other words, reading prototype of a proxy must always return the prototype of the
target object.undefinedundefined
Traps can intercept these operations, but they must follow these rules.
undefinedundefinedInvariants ensure correct and consistent behavior of language features. The full invariants list is in undefinedundefinedthe specification. You probably won't violate them if you're not doing something weird. undefinedundefined
undefinedundefinedLet's see how that works in practical examples.
undefinedundefinedThe most common traps are for reading/writing properties.
undefinedundefinedTo intercept reading, the undefinedundefinedhandler
should have a method undefinedundefinedget(target, property, receiver).undefinedundefined
It triggers when a property is read, with following arguments:
undefinedundefinedtarget - is the target object, the one passed as the first
argument to undefinedundefinednew Proxy,undefinedundefinedproperty - property name,undefinedundefinedreceiver - if the target property is a getter, then
undefinedundefinedreceiver is the object that's going to be used as undefinedundefinedthis
in its call. Usually that's the undefinedundefinedproxy object itself (or an object that inherits from
it, if we inherit from proxy). Right now we don't need this argument, so it will be explained in more detail
later.undefinedundefinedLet's use
undefinedundefinedget to implement default values for an object.undefinedundefined
We'll make a numeric array that returns undefinedundefined0 for nonexistent values.undefinedundefined
Usually when one tries to get a non-existing array item, they get
undefinedundefinedundefined, but we'll wrap a regular array into the proxy that traps reading and returns
undefinedundefined0 if there's no such property:undefinedundefined
run let numbers = [0, 1, 2];
undefinedundefinednumbers = new Proxy(numbers, { get(target, prop) { if (prop in target) { return target[prop]; } else { return 0; // default value } } });
undefinedundefinedundefinedundefined! alert( numbers[1] ); // 1 alert( numbers[123] ); // 0 (no such item) undefinedundefined/! undefinedundefined
undefinedundefinedAs we can see, it's quite easy to do with a
undefinedundefinedget trap.undefinedundefined
We can use
undefinedundefinedProxy to implement any logic for "default" values.undefinedundefined
Imagine we have a dictionary, with phrases and their translations:
undefinedundefinedrun let dictionary = { ‘Hello': ‘Hola', ‘Bye': ‘Adiós' };
undefinedundefinedalert( dictionary[‘Hello'] ); // Hola alert( dictionary[‘Welcome'] ); // undefined
undefinedundefinedRight now, if there's no phrase, reading from undefinedundefineddictionary returns
undefinedundefinedundefined. But in practice, leaving a phrase untranslated is usually better than
undefinedundefinedundefined. So let's make it return an untranslated phrase in that case instead of
undefinedundefinedundefined.undefinedundefined
To achieve that, we'll wrap
undefinedundefineddictionary in a proxy that intercepts reading operations:undefinedundefined
run let dictionary = { ‘Hello': ‘Hola', ‘Bye': ‘Adiós' };
undefinedundefineddictionary = new Proxy(dictionary, { undefinedundefined! get(target, phrase) { // intercept reading a property from dictionary undefinedundefined/! if (phrase in target) { // if we have it in the dictionary return target[phrase]; // return the translation } else { // otherwise, return the non-translated phrase return phrase; } } });undefinedundefined
undefinedundefined// Look up arbitrary phrases in the dictionary! // At worst, they're not translated. alert( dictionary[‘Hello'] ); // Hola undefinedundefined! alert( dictionary[‘Welcome to Proxy']); // Welcome to Proxy (no translation) undefinedundefined/! undefinedundefined
undefinedundefinedundefinedundefinedPlease note how the proxy overwrites the variable:
dictionary = new Proxy(dictionary, ...);
The proxy should totally replace the target object everywhere. No one should ever reference the target object after it got proxied. Otherwise it's easy to mess up.undefinedundefined
undefinedundefinedLet's say we want an array exclusively for numbers. If a value of another type is added, there should be an error.
undefinedundefinedThe undefinedundefinedset trap triggers when a property is written.undefinedundefined
undefinedundefinedset(target, property, value, receiver):undefinedundefined
target - is the target object, the one passed
as the first argument to undefinedundefinednew Proxy,undefinedundefinedproperty - property name,undefinedundefinedvalue - property value,undefinedundefinedreceiver - similar to undefinedundefinedget trap, matters only for
setter properties.undefinedundefinedThe
undefinedundefinedset trap should return undefinedundefinedtrue if setting is successful,
and undefinedundefinedfalse otherwise (triggers
undefinedundefinedTypeError).undefinedundefined
Let's use it to validate new values:
undefinedundefinedrun let numbers = [];
undefinedundefinednumbers = new Proxy(numbers, { // (undefinedundefined) !undefinedundefined set(target, prop, val) { // to intercept property writing /!* if (typeof val == ‘number') { target[prop] = val; return true; } else { return false; } } });undefinedundefined
undefinedundefinednumbers.push(1); // added successfully numbers.push(2); // added successfully alert("Length is:" + numbers.length); // 2
undefinedundefinedundefinedundefined! numbers.push("test"); // TypeError (‘set' on proxy returned false) undefinedundefined/!undefinedundefined
undefinedundefinedalert("This line is never reached (error in the line above)");
undefinedundefinedPlease note: the built-in functionality of arrays is still working! Values are added by
undefinedundefinedpush. The undefinedundefinedlength property auto-increases when values are
added. Our proxy doesn't break anything.undefinedundefined
We don't have to override
value-adding array methods like undefinedundefinedpush and undefinedundefinedunshift, and so
on, to add checks in there, because internally they use the undefinedundefined[[Set]] operation that's
intercepted by the proxy.undefinedundefined
So the code is clean and concise.
undefinedundefined``undefinedundefinedwarn header="Don't forget to returntrue`" As said above, there are
invariants to be held.undefinedundefined
For undefinedundefinedset, it must
return undefinedundefinedtrue for a successful write.undefinedundefined
If we
forget to do it or return any falsy value, the operation triggers undefinedundefinedTypeError.
undefinedundefined
undefinedundefinedObject.keys,
undefinedundefinedfor..in loop and most other methods that iterate over object properties use
undefinedundefined[[OwnPropertyKeys]] internal method (intercepted by
undefinedundefinedownKeys trap) to get a list of properties.undefinedundefined
Such methods differ in details: - undefinedundefinedObject.getOwnPropertyNames(obj) returns non-symbol
keys. - undefinedundefinedObject.getOwnPropertySymbols(obj) returns symbol keys. -
undefinedundefinedObject.keys/values() returns non-symbol keys/values with
undefinedundefinedenumerable flag (property flags were explained in the article undefinedundefinedinfo:property-descriptors). -
undefinedundefinedfor..in loops over non-symbol keys with undefinedundefinedenumerable flag,
and also prototype keys.undefinedundefined
…But all of them start with that list.
undefinedundefinedIn the example below we use undefinedundefinedownKeys trap to make
undefinedundefinedfor..in loop over undefinedundefineduser, and also
undefinedundefinedObject.keys and undefinedundefinedObject.values, to skip properties
starting with an underscore undefinedundefined_:undefinedundefined
run let user = { name: "John", age: 30, _password: "***" };
undefinedundefineduser = new Proxy(user, { undefinedundefined! ownKeys(target) { undefinedundefined/! return Object.keys(target).filter(key => !key.startsWith('_')); } });undefinedundefined
undefinedundefined// "ownKeys" filters out _password for(let key in user) alert(key); // name, then: age
undefinedundefined// same effect on these methods: alert( Object.keys(user) ); // name,age alert( Object.values(user) ); // John,30
undefinedundefinedSo far, it works.
undefinedundefinedAlthough, if we return a key that doesn't exist in
the object, undefinedundefinedObject.keys won't list it:undefinedundefined
run let user = { };
undefinedundefineduser = new Proxy(user, { undefinedundefined! ownKeys(target) { undefinedundefined/! return [‘a', ‘b', ‘c']; } });undefinedundefined
undefinedundefinedalert(
Object.keys(user) ); // undefinedundefined
Why? The reason is simple: undefinedundefinedObject.keys
returns only properties with the undefinedundefinedenumerable flag. To check for it, it calls the
internal method undefinedundefined[[GetOwnProperty]] for every property to get undefinedundefinedits descriptor. And here, as there's no property, its descriptor is empty, no
undefinedundefinedenumerable flag, so it's skipped.undefinedundefined
For
undefinedundefinedObject.keys to return a property, we need it to either exist in the object, with the
undefinedundefinedenumerable flag, or we can intercept calls to
undefinedundefined[[GetOwnProperty]] (the trap undefinedundefinedgetOwnPropertyDescriptor
does it), and return a descriptor with undefinedundefinedenumerable: true.undefinedundefined
Here's an example of that:
undefinedundefinedrun let user = { };
undefinedundefineduser = new Proxy(user, { ownKeys(target) { // called once to get a list of properties return [‘a', ‘b', ‘c']; },
undefinedundefinedgetOwnPropertyDescriptor(target, prop) { // called for every property return { enumerable: true, configurable: true /* …other flags, probable "value:…" */ }; }
undefinedundefined});
undefinedundefinedalert( Object.keys(user) ); // a, b, c
undefinedundefinedLet's note once again: we only need to intercept
undefinedundefined[[GetOwnProperty]] if the property is absent in the object.undefinedundefined
There's a widespread convention that properties and methods
prefixed by an underscore undefinedundefined_ are internal. They shouldn't be accessed from outside the
object.undefinedundefined
Technically that's possible though:
undefinedundefinedrun let user = { name: "John", _password: "secret" };
undefinedundefinedalert(user._password); // secret
undefinedundefinedLet's use proxies to prevent any access to properties starting with
undefinedundefined_.undefinedundefined
We'll need the traps: -
undefinedundefinedget to throw an error when reading such property, - undefinedundefinedset
to throw an error when writing, - undefinedundefineddeleteProperty to throw an error when deleting, -
undefinedundefinedownKeys to exclude properties starting with undefinedundefined_ from
undefinedundefinedfor..in and methods like undefinedundefinedObject.keys.undefinedundefined
Here's the code:
undefinedundefinedrun let user = { name: "John", _password: "***" };
undefinedundefineduser = new Proxy(user, { undefinedundefined! get(target, prop) { undefinedundefined/! if (prop.startsWith('_‘)) { throw new Error("Access denied"); } let value = target[prop]; return (typeof value === 'function') ? value.bind(target) : value; // (undefinedundefined) }, !undefinedundefined set(target, prop, val) { // to intercept property writing /!undefinedundefined if (prop.startsWith(‘undefinedundefined''')) { throw new Error("Access denied"); } else { target[prop] = val; return true; } }, undefinedundefined! deleteProperty(target, prop) { // to intercept property deletion undefinedundefined/! if (prop.startsWith('undefinedundefined''')) { throw new Error("Access denied"); } else { delete target[prop]; return true; } }, undefinedundefined!undefinedundefined ownKeys(target) { // to intercept property list /!* return Object.keys(target).filter(key => !key.startsWith('_')); } });undefinedundefined
undefinedundefined// "get" doesn't allow to read _password try { alert(user._password); // Error: Access denied } catch(e) { alert(e.message); }
undefinedundefined// "set" doesn't allow to write _password try { user._password = "test"; // Error: Access denied } catch(e) { alert(e.message); }
undefinedundefined// "deleteProperty" doesn't allow to delete _password try { delete user._password; // Error: Access denied } catch(e) { alert(e.message); }
undefinedundefined// "ownKeys" filters out _password for(let key in user) alert(key); // name
undefinedundefinedPlease note the important detail in the undefinedundefinedget trap, in the line
undefinedundefined(*):undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedget(targetundefinedundefined, prop) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefinedlet value undefinedundefined= target[prop]undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedreturn (undefinedundefinedtypeof value undefinedundefined===undefinedundefined'function') undefinedundefined?undefinedundefinedvalue.undefinedundefinedbind(target) : valueundefinedundefined;undefinedundefined// (*)undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Why do we need a function to call
undefinedundefinedvalue.bind(target)?undefinedundefined
The reason is that object
methods, such as undefinedundefineduser.checkPassword(), must be able to access
undefinedundefined_password:undefinedundefined
undefinedundefinedundefinedundefineduser undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// ...undefinedundefinedundefinedundefinedundefinedundefinedcheckPassword(value) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined// object method must be able to read _passwordundefinedundefinedundefinedundefinedundefinedundefinedreturn value undefinedundefined===undefinedundefinedthis.undefinedundefined_passwordundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
A call to undefinedundefineduser.checkPassword() gets proxied
undefinedundefineduser as undefinedundefinedthis (the object before dot becomes
undefinedundefinedthis), so when it tries to access undefinedundefinedthis._password, the
undefinedundefinedget trap activates (it triggers on any property read) and throws an
error.undefinedundefined
So we bind the context of object methods to the original object,
undefinedundefinedtarget, in the line undefinedundefined(*). Then their future calls will
use undefinedundefinedtarget as undefinedundefinedthis, without any traps.undefinedundefined
That solution usually works, but isn't ideal, as a method may pass the unproxied object somewhere else, and then we'll get messed up: where's the original object, and where's the proxied one?
undefinedundefinedBesides, an object may be proxied multiple times (multiple proxies may add different "tweaks" to the object), and if we pass an unwrapped object to a method, there may be unexpected consequences.
undefinedundefinedSo, such a proxy shouldn't be used everywhere.
undefinedundefined
``undefinedundefinedsmart header="Private properties of a class" Modern JavaScript engines natively support private properties in classes, prefixed with#`.
They are described in the article undefinedundefinedinfo:private-protected-properties-methods. No proxies required.undefinedundefined
Such properties have their own issues though. In particular, they are not inherited.
undefinedundefinedLet's see more examples.
undefinedundefinedWe have a range object:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet range undefinedundefined=undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedstartundefinedundefined:undefinedundefined1undefinedundefined,undefinedundefinedundefinedundefinedundefinedundefinedendundefinedundefined:undefinedundefined10undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
We'd like to use the undefinedundefinedin operator to check that a number is in
undefinedundefinedrange.undefinedundefined
The undefinedundefinedhas
trap intercepts undefinedundefinedin calls.undefinedundefined
undefinedundefinedhas(target, property)undefinedundefined
target - is the target object, passed as the first argument to
undefinedundefinednew Proxy,undefinedundefinedproperty - property nameundefinedundefinedHere's the demo:
undefinedundefinedrun let range = { start: 1, end: 10 };
undefinedundefinedrange = new Proxy(range, { undefinedundefined! has(target, prop) { undefinedundefined/! return prop >= target.start && prop <= target.end; } });undefinedundefined
undefinedundefinedundefinedundefined! alert(5 in range); // true alert(50 in range); // false undefinedundefined/! undefinedundefined
undefinedundefinedNice syntactic sugar, isn't it? And very simple to implement.
undefinedundefinedWe can wrap a proxy around a function as well.
undefinedundefinedThe
undefinedundefinedapply(target, thisArg, args) trap handles calling a proxy as
function:undefinedundefined
target is
the target object (function is an object in JavaScript),undefinedundefinedthisArg is the value of undefinedundefinedthis.undefinedundefinedargs is a list of arguments.undefinedundefinedFor example, let's recall undefinedundefineddelay(f, ms)
decorator, that we did in the article undefinedundefinedinfo:call-apply-decorators.undefinedundefined
In that article we did it
without proxies. A call to undefinedundefineddelay(f, ms) returned a function that forwards all calls to
undefinedundefinedf after undefinedundefinedms milliseconds.undefinedundefined
Here's the previous, function-based implementation:
undefinedundefinedrun function delay(f, ms) { // return a wrapper that passes the call to f after the timeout return function() { // (*) setTimeout(() => f.apply(this, arguments), ms); }; }
undefinedundefinedfunction sayHi(user) {
alert(undefinedundefinedHello, ${user}!); }undefinedundefined
// after this wrapping, calls to sayHi will be delayed for 3 seconds sayHi = delay(sayHi, 3000);
undefinedundefinedsayHi("John"); // Hello, John! (after 3 seconds)
undefinedundefinedAs we've seen already, that mostly works. The wrapper function
undefinedundefined(*) performs the call after the timeout.undefinedundefined
But
a wrapper function does not forward property read/write operations or anything else. After the wrapping, the access is
lost to properties of the original functions, such as undefinedundefinedname,
undefinedundefinedlength and others:undefinedundefined
run function delay(f, ms) { return function() { setTimeout(() => f.apply(this, arguments), ms); }; }
undefinedundefinedfunction
sayHi(user) { alert(undefinedundefinedHello, ${user}!); }undefinedundefined
undefinedundefined! alert(sayHi.length); // 1 (function length is the arguments count in its declaration) undefinedundefined/!undefinedundefined
undefinedundefinedsayHi = delay(sayHi, 3000);
undefinedundefinedundefinedundefined! alert(sayHi.length); // 0 (in the wrapper declaration, there are zero arguments) undefinedundefined/! undefinedundefined
undefinedundefinedundefinedundefinedProxy is much more powerful, as it forwards
everything to the target object.undefinedundefined
Let's use
undefinedundefinedProxy instead of a wrapping function:undefinedundefined
run function delay(f, ms) { return new Proxy(f, { apply(target, thisArg, args) { setTimeout(() => target.apply(thisArg, args), ms); } }); }
undefinedundefinedfunction sayHi(user) {
alert(undefinedundefinedHello, ${user}!); }undefinedundefined
sayHi = delay(sayHi, 3000);
undefinedundefinedundefinedundefined! alert(sayHi.length); // 1 (undefinedundefined) proxy forwards "get length" operation to the target /!*undefinedundefined
undefinedundefinedsayHi("John"); // Hello, John! (after 3 seconds)
undefinedundefinedThe result is the same, but now not only calls, but all operations on the proxy are forwarded
to the original function. So undefinedundefinedsayHi.length is returned correctly after the wrapping in
the line undefinedundefined(*).undefinedundefined
We've got a "richer" wrapper.
undefinedundefinedOther traps exist: the full list is in the beginning of this article. Their usage pattern is similar to the above.
undefinedundefined
undefinedundefinedReflect is a built-in object that simplifies creation of
undefinedundefinedProxy.undefinedundefined
It was said previously that internal
methods, such as undefinedundefined[[Get]], undefinedundefined[[Set]] and others are
specification-only, they can't be called directly.undefinedundefined
The
undefinedundefinedReflect object makes that somewhat possible. Its methods are minimal wrappers around
the internal methods.undefinedundefined
Here are examples of operations and
undefinedundefinedReflect calls that do the same:undefinedundefined
| Operation | undefinedundefinedundefinedundefinedReflect callundefinedundefined | undefinedundefinedInternal method | undefinedundefined
|---|---|---|
undefinedundefinedobj[prop]undefinedundefined | undefinedundefined
undefinedundefinedReflect.get(obj, prop)undefinedundefined | undefinedundefined
undefinedundefined[[Get]]undefinedundefined | undefinedundefined
undefinedundefinedobj[prop] = valueundefinedundefined |
undefinedundefinedundefinedundefinedReflect.set(obj, prop, value)undefinedundefined |
undefinedundefinedundefinedundefined[[Set]]undefinedundefined | undefinedundefined
undefinedundefineddelete obj[prop]undefinedundefined | undefinedundefined
undefinedundefinedReflect.deleteProperty(obj, prop)undefinedundefined | undefinedundefined
undefinedundefined[[Delete]]undefinedundefined | undefinedundefined
undefinedundefinednew F(value)undefinedundefined |
undefinedundefinedundefinedundefinedReflect.construct(F, value)undefinedundefined |
undefinedundefinedundefinedundefined[[Construct]]undefinedundefined | undefinedundefined
| … | undefinedundefined… | undefinedundefined… | undefinedundefined
For example:
undefinedundefinedrun let user = {};
undefinedundefinedReflect.set(user, ‘name', ‘John');
undefinedundefinedalert(user.name); // John
undefinedundefinedIn particular, undefinedundefinedReflect allows us to call operators
(undefinedundefinednew, undefinedundefineddelete…) as functions
(undefinedundefinedReflect.construct, undefinedundefinedReflect.deleteProperty, …). That's
an interesting capability, but here another thing is important.undefinedundefined
undefinedundefinedFor every internal method, trappable by undefinedundefinedProxy, there's a
corresponding method in undefinedundefinedReflect, with the same name and arguments as the
undefinedundefinedProxy trap.undefinedundefinedundefinedundefined
So
we can use undefinedundefinedReflect to forward an operation to the original object.undefinedundefined
In this example, both traps undefinedundefinedget and
undefinedundefinedset transparently (as if they didn't exist) forward reading/writing operations to the
object, showing a message:undefinedundefined
run let user = { name: "John", };
undefinedundefineduser = new Proxy(user, { get(target, prop, receiver) {
alert(undefinedundefinedGET ${prop}); undefinedundefined! return Reflect.get(target, prop,
receiver); // (1) undefinedundefined/! }, set(target, prop, val, receiver) {
alert(undefinedundefinedSET ${prop}=${val}); undefinedundefined! return Reflect.set(target,
prop, val, receiver); // (2) undefinedundefined/! } });undefinedundefined
let name = user.name; // shows "GET name" user.name = "Pete"; // shows "SET name=Pete"
undefinedundefinedHere:
undefinedundefinedReflect.get
reads an object property.undefinedundefinedReflect.set
writes an object property and returns undefinedundefinedtrue if successful,
undefinedundefinedfalse otherwise.undefinedundefined
That is, everything's simple: if a trap wants to forward the call to the object, it's enough to call
undefinedundefinedReflect.<method> with the same arguments.undefinedundefined
In most cases we can do the same without undefinedundefinedReflect, for instance, reading a property
undefinedundefinedReflect.get(target, prop, receiver) can be replaced by
undefinedundefinedtarget[prop]. There are important nuances though.undefinedundefined
Let's see an example that
demonstrates why undefinedundefinedReflect.get is better. And we'll also see why
undefinedundefinedget/set have the third argument undefinedundefinedreceiver, that we didn't
use before.undefinedundefined
We have an object undefinedundefineduser with
undefinedundefined_name property and a getter for it.undefinedundefined
Here's a proxy around it:
undefinedundefinedrun let user = { _name: "Guest", get name() { return this._name; } };
undefinedundefinedundefinedundefined! let userProxy = new Proxy(user, { get(target, prop, receiver) { return target[prop]; } }); undefinedundefined/!undefinedundefined
undefinedundefinedalert(userProxy.name); // Guest
undefinedundefinedThe undefinedundefinedget trap is "transparent" here, it returns the original
property, and doesn't do anything else. That's enough for our example.undefinedundefined
Everything seems to be all right. But let's make the example a little bit more complex.
undefinedundefinedAfter
inheriting another object undefinedundefinedadmin from undefinedundefineduser, we can
observe the incorrect behavior:undefinedundefined
run let user = { _name: "Guest", get name() { return this._name; } };
undefinedundefinedlet userProxy = new Proxy(user, { get(target, prop, receiver) { return target[prop]; // (*) target = user } });
undefinedundefinedundefinedundefined! let admin = { undefinedundefinedproto: userProxy, _name: "Admin" };undefinedundefined
undefinedundefined// Expected: Admin alert(admin.name); // outputs: Guest (?!?) undefinedundefined/! undefinedundefined
undefinedundefinedReading undefinedundefinedadmin.name should return
undefinedundefined"Admin", not undefinedundefined"Guest"!undefinedundefined
What's the matter? Maybe we did something wrong with the inheritance?
undefinedundefinedBut if we remove the proxy, then everything will work as expected.
undefinedundefinedThe problem is actually in the
proxy, in the line undefinedundefined(*).undefinedundefined
admin.name, as undefinedundefinedadmin
object doesn't have such own property, the search goes to its prototype.undefinedundefineduserProxy.undefinedundefinedWhen reading undefinedundefinedname property from the proxy, its
undefinedundefinedget trap triggers and returns it from the original object as
undefinedundefinedtarget[prop] in the line undefinedundefined(*).undefinedundefined
A call to undefinedundefinedtarget[prop], when undefinedundefinedprop
is a getter, runs its code in the context undefinedundefinedthis=target. So the result is
undefinedundefinedthis._name from the original object undefinedundefinedtarget, that is:
from undefinedundefineduser.undefinedundefined
To fix such situations, we need undefinedundefinedreceiver, the third argument of
undefinedundefinedget trap. It keeps the correct undefinedundefinedthis to be passed to a
getter. In our case that's undefinedundefinedadmin.undefinedundefined
How to pass
the context for a getter? For a regular function we could use undefinedundefinedcall/apply, but that's a
getter, it's not "called", just accessed.undefinedundefined
undefinedundefinedReflect.get can do that. Everything will work right if we use it.undefinedundefined
Here's the corrected variant:
undefinedundefinedrun let user = { _name: "Guest", get name() { return this._name; } };
undefinedundefinedlet userProxy = new Proxy(user, { get(target, prop, receiver) { // receiver = admin undefinedundefined! return Reflect.get(target, prop, receiver); // (undefinedundefined) /!* } });undefinedundefined
undefinedundefinedlet admin = { undefinedundefinedproto: userProxy, _name: "Admin" };undefinedundefined
undefinedundefinedundefinedundefined! alert(admin.name); // Admin undefinedundefined/! undefinedundefined
undefinedundefinedNow undefinedundefinedreceiver that keeps a reference to the
correct undefinedundefinedthis (that is undefinedundefinedadmin), is passed to the getter
using undefinedundefinedReflect.get in the line undefinedundefined(*).undefinedundefined
We can rewrite the trap even shorter:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedget(targetundefinedundefined, propundefinedundefined, receiver) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedReflect.undefinedundefinedget(undefinedundefined*!*...undefinedundefinedargumentsundefinedundefined*undefinedundefined/!undefinedundefined*)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedReflect calls are named exactly the same way as traps and
accept the same arguments. They were specifically designed this way.undefinedundefined
So,
undefinedundefinedreturn Reflect... provides a safe no-brainer to forward the operation and make sure we
don't forget anything related to that.undefinedundefined
Proxies provide a unique way to alter or tweak the behavior of the existing objects at the lowest level. Still, it's not perfect. There are limitations.
undefinedundefinedMany built-in objects,
for example undefinedundefinedMap, undefinedundefinedSet,
undefinedundefinedDate, undefinedundefinedPromise and others make use of so-called "internal
slots".undefinedundefined
These are like properties, but reserved for internal,
specification-only purposes. For instance, undefinedundefinedMap stores items in the internal slot
undefinedundefined[[MapData]]. Built-in methods access them directly, not via
undefinedundefined[[Get]]/[[Set]] internal methods. So undefinedundefinedProxy can't
intercept that.undefinedundefined
Why care? They're internal anyway!
undefinedundefinedWell, here's the issue. After a built-in object like that gets proxied, the proxy doesn't have these internal slots, so built-in methods will fail.
undefinedundefinedFor example:
undefinedundefinedrun let map = new Map();
undefinedundefinedlet proxy = new Proxy(map, {});
undefinedundefinedundefinedundefined! proxy.set(‘test', 1); // Error undefinedundefined/! undefinedundefined
undefinedundefinedInternally, a undefinedundefinedMap stores all data in its
undefinedundefined[[MapData]] internal slot. The proxy doesn't have such a slot. The undefinedundefinedbuilt-in method
undefinedundefinedMap.prototype.setundefinedundefined method tries to access the internal property
undefinedundefinedthis.[[MapData]], but because undefinedundefinedthis=proxy, can't find it
in undefinedundefinedproxy and just fails.undefinedundefined
Fortunately, there's a way to fix it:
undefinedundefinedrun let map = new Map();
undefinedundefinedlet proxy = new Proxy(map, { get(target, prop, receiver) { let value = Reflect.get(…arguments); undefinedundefined! return typeof value == ‘function' ? value.bind(target) : value; undefinedundefined/! } });undefinedundefined
undefinedundefinedproxy.set(‘test', 1); alert(proxy.get(‘test')); // 1 (works!)
undefinedundefinedNow it works fine, because undefinedundefinedget trap binds function properties,
such as undefinedundefinedmap.set, to the target object (undefinedundefinedmap)
itself.undefinedundefined
Unlike the previous example, the value of
undefinedundefinedthis inside undefinedundefinedproxy.set(...) will be not
undefinedundefinedproxy, but the original undefinedundefinedmap. So when the internal
implementation of undefinedundefinedset tries to access undefinedundefinedthis.[[MapData]]
internal slot, it succeeds.undefinedundefined
``undefinedundefinedsmart header="Arrayundefinedundefinedhas no internal slots" A notable exception: built-inArray`
doesn't use internal slots. That's for historical reasons, as it appeared so long ago.undefinedundefined
So there's no such problem when proxying an array.
undefinedundefinedA similar thing happens with private class fields.
undefinedundefinedFor example, undefinedundefinedgetName() method accesses
the private undefinedundefined#name property and breaks after proxying:undefinedundefined
run class User { #name = "Guest";
undefinedundefinedgetName() { return this.#name; } }
undefinedundefinedlet user = new User();
undefinedundefineduser = new Proxy(user, {});
undefinedundefinedundefinedundefined! alert(user.getName()); // Error undefinedundefined/! undefinedundefined
undefinedundefinedThe reason is that private fields are implemented using internal slots.
JavaScript does not use undefinedundefined[[Get]]/[[Set]] when accessing them.undefinedundefined
In the call undefinedundefinedgetName() the value of
undefinedundefinedthis is the proxied undefinedundefineduser, and it doesn't have the slot
with private fields.undefinedundefined
Once again, the solution with binding the method makes it work:
undefinedundefinedrun class User { #name = "Guest";
undefinedundefinedgetName() { return this.#name; } }
undefinedundefinedlet user = new User();
undefinedundefineduser = new Proxy(user, { get(target, prop, receiver) { let value = Reflect.get(…arguments); return typeof value == ‘function' ? value.bind(target) : value; } });
undefinedundefinedalert(user.getName()); // Guest
undefinedundefinedThat said, the solution has drawbacks, as explained previously: it exposes the original object to the method, potentially allowing it to be passed further and breaking other proxied functionality.
undefinedundefinedThe proxy and the original object are different objects. That's natural, right?
undefinedundefinedSo if we use the original object as a key, and then proxy it, then the proxy can't be found:
undefinedundefinedrun let allUsers = new Set();
undefinedundefinedclass User { constructor(name) { this.name = name; allUsers.add(this); } }
undefinedundefinedlet user = new User("John");
undefinedundefinedalert(allUsers.has(user)); // true
undefinedundefineduser = new Proxy(user, {});
undefinedundefinedundefinedundefined! alert(allUsers.has(user)); // false undefinedundefined/! undefinedundefined
undefinedundefinedAs we can see, after proxying we can't find
undefinedundefineduser in the set undefinedundefinedallUsers, because the proxy is a
different object.undefinedundefined
``undefinedundefinedwarn header="Proxies can't intercept a strict equality test===undefinedundefined" Proxies can intercept many operators, such asnewundefinedundefined(withconstructundefinedundefined),inundefinedundefined(withhasundefinedundefined),deleteundefinedundefined(withdeleteProperty`)
and so on.undefinedundefined
But there's no way to intercept a strict equality test for objects. An object is strictly equal to itself only, and no other value.
undefinedundefinedSo all operations and built-in classes that compare objects for equality will differentiate between the object and the proxy. No transparent replacement here.
undefinedundefinedA undefinedundefinedrevocable proxy is a proxy that can be disabled.undefinedundefined
undefinedundefinedLet's say we have a resource, and would like to close access to it any moment.
undefinedundefinedWhat we can do is to wrap it into a revocable proxy, without any traps. Such a proxy will forward operations to object, and we can disable it at any moment.
undefinedundefinedThe syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedletundefinedundefined{proxyundefinedundefined, revokeundefinedundefined}undefinedundefined=undefinedundefinedProxy.undefinedundefinedrevocable(targetundefinedundefined, handler)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The call returns an object with the undefinedundefinedproxy and
undefinedundefinedrevoke function to disable it.undefinedundefined
Here's an example:
undefinedundefinedrun let object = { data: "Valuable data" };
undefinedundefinedlet {proxy, revoke} = Proxy.revocable(object, {});
undefinedundefined// pass the proxy somewhere instead of object… alert(proxy.data); // Valuable data
undefinedundefined// later in our code revoke();
undefinedundefined// the proxy isn't working any more (revoked) alert(proxy.data); // Error
undefinedundefinedA call to undefinedundefinedrevoke() removes all internal references to the target
object from the proxy, so they are no longer connected.undefinedundefined
Initially,
undefinedundefinedrevoke is separate from undefinedundefinedproxy, so that we can pass
undefinedundefinedproxy around while leaving undefinedundefinedrevoke in the current
scope.undefinedundefined
We can also bind undefinedundefinedrevoke method to
proxy by setting undefinedundefinedproxy.revoke = revoke.undefinedundefined
Another option is to create a undefinedundefinedWeakMap that has undefinedundefinedproxy as
the key and the corresponding undefinedundefinedrevoke as the value, that allows to easily find
undefinedundefinedrevoke for a proxy:undefinedundefined
run undefinedundefined! let revokes = new WeakMap(); undefinedundefined/!undefinedundefined
undefinedundefinedlet object = { data: "Valuable data" };
undefinedundefinedlet {proxy, revoke} = Proxy.revocable(object, {});
undefinedundefinedrevokes.set(proxy, revoke);
undefinedundefined// ..somewhere else in our code.. revoke = revokes.get(proxy); revoke();
undefinedundefinedalert(proxy.data); // Error (revoked)
undefinedundefinedWe use undefinedundefinedWeakMap instead of undefinedundefinedMap
here because it won't block garbage collection. If a proxy object becomes "unreachable" (e.g. no variable references
it any more), undefinedundefinedWeakMap allows it to be wiped from memory together with its
undefinedundefinedrevoke that we won't need any more.undefinedundefined
undefinedundefinedProxy is a wrapper
around an object, that forwards operations on it to the object, optionally trapping some of them.undefinedundefined
It can wrap any kind of object, including classes and functions.
undefinedundefinedThe syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet proxy undefinedundefined=undefinedundefinednewundefinedundefinedProxy(targetundefinedundefined,undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefined/* traps */undefinedundefinedundefinedundefinedundefinedundefined})undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…Then we should use undefinedundefinedproxy everywhere instead of
undefinedundefinedtarget. A proxy doesn't have its own properties or methods. It traps an operation if
the trap is provided, otherwise forwards it to undefinedundefinedtarget object.undefinedundefined
We can trap: - Reading (undefinedundefinedget), writing
(undefinedundefinedset), deleting (undefinedundefineddeleteProperty) a property (even a
non-existing one). - Calling a function (undefinedundefinedapply trap). - The
undefinedundefinednew operator (undefinedundefinedconstruct trap). - Many other operations
(the full list is at the beginning of the article and in the undefinedundefineddocs).undefinedundefined
That allows us to create "virtual" properties and methods, implement default values, observable objects, function decorators and so much more.
undefinedundefinedWe can also wrap an object multiple times in different proxies, decorating it with various aspects of functionality.
undefinedundefinedThe undefinedundefinedReflect API is designed to complement
undefinedundefinedProxy. For any
undefinedundefinedProxy trap, there's a undefinedundefinedReflect call with same arguments.
We should use those to forward calls to target objects.undefinedundefined
Proxies have some limitations:
undefinedundefinedthis to access them.undefinedundefined=== can't be intercepted.undefinedundefinedThe built-in
undefinedundefinedeval function allows to execute a string of code.undefinedundefined
The syntax is:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet result undefinedundefined=undefinedundefinedeval(code)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
For example:
undefinedundefined
undefinedundefinedjs run let code = 'alert("Hello")'; eval(code); // Helloundefinedundefined
A string of code may be long, contain line breaks, function declarations, variables and so on.
undefinedundefinedThe result of undefinedundefinedeval is the result of the last
statement.undefinedundefined
For example:
undefinedundefinedjs run let value = eval('1+1'); alert(value); // 2undefinedundefined
undefinedundefinedjs run let value = eval('let i = 0; ++i'); alert(value); // 1undefinedundefined
The eval'ed code is executed in the current lexical environment, so it can see outer variables:
undefinedundefinedrun no-beautify let a = 1;
undefinedundefinedfunction f() { let a = 2;
undefinedundefinedundefinedundefined! eval(‘alert(a)'''); // 2 undefinedundefined/! }undefinedundefined
undefinedundefinedf();
undefinedundefinedIt can change outer variables as well:
undefinedundefined
undefinedundefinedjs untrusted refresh run let x = 5; eval("x = 10"); alert(x); // 10, value modifiedundefinedundefined
In strict mode, undefinedundefinedeval has its own lexical environment. So
functions and variables, declared inside eval, are not visible outside:undefinedundefined
untrusted refresh run // reminder: ‘use strict' is enabled in runnable examples by default
undefinedundefinedeval("let x = 5; function f() {}");
undefinedundefinedalert(typeof x); // undefined (no such variable) // function f is also not visible
undefinedundefinedWithout undefinedundefineduse strict, undefinedundefinedeval doesn't
have its own lexical environment, so we would see undefinedundefinedx and
undefinedundefinedf outside.undefinedundefined
In modern programming undefinedundefinedeval is used very sparingly. It's often said
that "eval is evil".undefinedundefined
The reason is simple: long, long time ago JavaScript
was a much weaker language, many things could only be done with undefinedundefinedeval. But that time
passed a decade ago.undefinedundefined
Right now, there's almost no reason to use
undefinedundefinedeval. If someone is using it, there's a good chance they can replace it with a modern
language construct or a undefinedundefinedJavaScript Module.undefinedundefined
Please note that its ability to access outer variables has side-effects.
undefinedundefined
Code minifiers (tools used before JS gets to production, to compress it) rename local variables into shorter ones
(like undefinedundefineda, undefinedundefinedb etc) to make the code smaller. That's usually
safe, but not if undefinedundefinedeval is used, as local variables may be accessed from eval'ed code
string. So minifiers don't do that renaming for all variables potentially visible from
undefinedundefinedeval. That negatively affects code compression ratio.undefinedundefined
Using outer local variables inside undefinedundefinedeval is also considered a bad
programming practice, as it makes maintaining the code more difficult.undefinedundefined
There are two ways how to be totally safe from such problems.
undefinedundefinedundefinedundefinedIf eval'ed
code doesn't use outer variables, please call undefinedundefinedeval as
undefinedundefinedwindow.eval(...):undefinedundefinedundefinedundefined
This way the code is executed in the global scope:
undefinedundefined
undefinedundefinedjs untrusted refresh run let x = 1; { let x = 5; window.eval('alert(x)'); // 1 (global variable) }undefinedundefined
undefinedundefinedIf eval'ed code needs local variables, change
undefinedundefinedeval to undefinedundefinednew Function and pass them as
arguments:undefinedundefinedundefinedundefined
run let f = new Function(‘a', ‘alert(a)''');
undefinedundefinedf(5); // 5
undefinedundefinedThe undefinedundefinednew Function construct is explained in the chapter
undefinedundefinedinfo:new-function. It creates a function from a string,
also in the global scope. So it can't see local variables. But it's so much clearer to pass them explicitly as
arguments, like in the example above.undefinedundefined
A call to undefinedundefinedeval(code) runs the string of code and returns the result
of the last statement. - Rarely used in modern JavaScript, as there's usually no need. - Can access outer local
variables. That's considered bad practice. - Instead, to undefinedundefinedeval the code in the global
scope, use undefinedundefinedwindow.eval(code). - Or, if your code needs some data from the outer scope,
use undefinedundefinednew Function and pass it as arguments.undefinedundefined
A value in JavaScript is always of a certain type. For example, a string or a number.
undefinedundefinedThere are eight basic data types in JavaScript. Here, we'll cover them in general and in the next chapters we'll talk about each of them in detail.
undefinedundefinedWe can put any type in a variable. For example, a variable can at one moment be a string and then store a number:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// no errorundefinedundefinedundefinedundefinedundefinedundefinedlet message undefinedundefined=undefinedundefined"hello"undefinedundefined;undefinedundefinedundefinedundefinedmessage undefinedundefined=undefinedundefined123456undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Programming languages that allow such things, such as JavaScript, are called "dynamically typed", meaning that there exist data types, but variables are not bound to any of them.
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet n undefinedundefined=undefinedundefined123undefinedundefined;undefinedundefinedundefinedundefinedn undefinedundefined=undefinedundefined12.345undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The undefinedundefinednumber type represents both integer and floating point numbers.undefinedundefined
undefinedundefinedThere are many operations for numbers, e.g. multiplication
undefinedundefined*, division undefinedundefined/, addition
undefinedundefined+, subtraction undefinedundefined-, and so on.undefinedundefined
Besides regular numbers, there are so-called "special numeric values" which also belong to this
data type: undefinedundefinedInfinity, undefinedundefined-Infinity and
undefinedundefinedNaN.undefinedundefined
undefinedundefinedInfinity represents the mathematical undefinedundefinedInfinity ∞. It is a special value that's greater than any
number.undefinedundefined
We can get it as a result of division by zero:
undefinedundefinedundefinedundefinedjs run alert( 1 / 0 ); // Infinityundefinedundefined
Or just reference it directly:
undefinedundefinedjs run alert( Infinity ); // InfinityundefinedundefinedundefinedundefinedNaN represents a computational error. It is a result of an
incorrect or an undefined mathematical operation, for instance:undefinedundefined
undefinedundefinedjs run alert( "not a number" / 2 ); // NaN, such division is erroneousundefinedundefined
undefinedundefinedNaN is sticky. Any further operation on
undefinedundefinedNaN returns undefinedundefinedNaN:undefinedundefined
undefinedundefinedjs run alert( "not a number" / 2 + 5 ); // NaNundefinedundefined
So, if there's a undefinedundefinedNaN somewhere in a mathematical expression, it
propagates to the whole result.undefinedundefined
smart header="Mathematical operations are safe" Doing maths is "safe" in JavaScript. We can do anything: divide by zero, treat non-numeric strings as numbers, etc.
undefinedundefinedThe script will never stop with a fatal error
("die"). At worst, we'll get undefinedundefinedNaN as the result.
undefinedundefined
Special numeric values formally belong to the "number" type. Of course they are not numbers in the common sense of this word.
undefinedundefinedWe'll see more about working with numbers in the chapter undefinedundefinedinfo:number.undefinedundefined
undefinedundefinedIn JavaScript, the "number" type cannot represent
integer values larger than undefinedundefined(2undefinedundefined53-1)undefinedundefined
(that's undefinedundefined9007199254740991), or less than
undefinedundefined-(2undefinedundefined53-1)undefinedundefined for negatives. It's a technical
limitation caused by their internal representation.undefinedundefined
For most purposes that's quite enough, but sometimes we need really big numbers, e.g. for cryptography or microsecond-precision timestamps.
undefinedundefinedundefinedundefinedBigInt type was recently added to the language to represent integers
of arbitrary length.undefinedundefined
A undefinedundefinedBigInt value is
created by appending undefinedundefinedn to the end of an integer:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// the "n" at the end means it's a BigIntundefinedundefinedundefinedundefinedundefinedundefinedconst bigInt undefinedundefined= 1234567890123456789012345678901234567890nundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
As undefinedundefinedBigInt numbers are rarely needed, we don't cover them here,
but devoted them a separate chapter undefinedundefinedinfo:bigint. Read it when
you need such big numbers.undefinedundefined
undefinedundefinedsmart header="Compatibility issues" Right now, `BigInt` is supported in Firefox/Chrome/Edge/Safari, but not in IE.undefinedundefined
You can check undefinedundefinedundefinedundefinedMDN BigInt compatibility tableundefinedundefined to know which versions of a browser are supported.undefinedundefined
undefinedundefinedA string in JavaScript must be surrounded by quotes.
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet str undefinedundefined=undefinedundefined"Hello"undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedlet str2 undefinedundefined=undefinedundefined'Single quotes are ok too'undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedlet phrase undefinedundefined=undefinedundefined`can embed another undefinedundefined${strundefinedundefined}undefinedundefined`undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In JavaScript, there are 3 types of quotes.
undefinedundefined"Hello".undefinedundefined'Hello'.undefinedundefined`Hello`.undefinedundefinedDouble and single quotes are "simple" quotes. There's practically no difference between them in JavaScript.
undefinedundefinedBackticks are "extended functionality" quotes. They allow us to embed variables and expressions into a string by
wrapping them in undefinedundefined${…}, for example:undefinedundefined
run let name = "John";
undefinedundefined// embed a variable alert(
undefinedundefinedHello, *!*${name}*/!*! ); // Hello, John!undefinedundefined
//
embed an expression alert( undefinedundefinedthe result is *!*${1 + 2}*/!* ); // the result is 3
undefinedundefined
The expression inside undefinedundefined${…} is evaluated and
the result becomes a part of the string. We can put anything in there: a variable like
undefinedundefinedname or an arithmetical expression like undefinedundefined1 + 2 or
something more complex.undefinedundefined
Please note that this can only be done in backticks.
Other quotes don't have this embedding functionality!
undefinedundefinedjs run alert( "the result is ${1 + 2}" ); // the result is ${1 + 2} (double quotes do nothing)undefinedundefined
We'll cover strings more thoroughly in the chapter undefinedundefinedinfo:string.undefinedundefined
undefinedundefinedsmart header="There is no undefinedundefinedcharacter type." In some languages, there is a special "character" type for a single character. For example, in the C language and in Java it is called "char".undefinedundefined
undefinedundefinedIn JavaScript, there is no such type. There's only one type:
undefinedundefinedstring. A string may consist of zero characters (be empty), one character or many of
them.
undefinedundefined
The boolean type has only two values: undefinedundefinedtrue and
undefinedundefinedfalse.undefinedundefined
This type is commonly used to store
yes/no values: undefinedundefinedtrue means "yes, correct", and undefinedundefinedfalse
means "no, incorrect".undefinedundefined
For instance:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet nameFieldChecked undefinedundefined=undefinedundefinedtrueundefinedundefined;undefinedundefined// yes, name field is checkedundefinedundefinedundefinedundefinedundefinedundefinedlet ageFieldChecked undefinedundefined=undefinedundefinedfalseundefinedundefined;undefinedundefined// no, age field is not checkedundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Boolean values also come as a result of comparisons:
undefinedundefinedrun let isGreater = 4 > 1;
undefinedundefinedalert( isGreater ); // true (the comparison result is "yes")
undefinedundefinedWe'll cover booleans more deeply in the chapter undefinedundefinedinfo:logical-operators.undefinedundefined
undefinedundefinedThe special undefinedundefinednull value
does not belong to any of the types described above.undefinedundefined
It forms a separate
type of its own which contains only the undefinedundefinednull value:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlet age undefinedundefined=undefinedundefinednullundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
In JavaScript, undefinedundefinednull is not a "reference to a non-existing
object" or a "null pointer" like in some other languages.undefinedundefined
It's just a special value which represents "nothing", "empty" or "value unknown".
undefinedundefinedThe code above states
that undefinedundefinedage is unknown.undefinedundefined
The special value
undefinedundefinedundefined also stands apart. It makes a type of its own, just like
undefinedundefinednull.undefinedundefined
The meaning of
undefinedundefinedundefined is "value is not assigned".undefinedundefined
If a
variable is declared, but not assigned, then its value is undefinedundefinedundefined:undefinedundefined
run let age;
undefinedundefinedalert(age); // shows "undefined"
undefinedundefinedTechnically, it is possible to explicitly assign undefinedundefinedundefined to a
variable:undefinedundefined
run let age = 100;
undefinedundefined// change the value to undefined age = undefined;
undefinedundefinedalert(age); // "undefined"
undefinedundefined…But we don't recommend doing that. Normally, one uses undefinedundefinednull to
assign an "empty" or "unknown" value to a variable, while undefinedundefinedundefined is reserved as a
default initial value for unassigned things.undefinedundefined
The undefinedundefinedobject type is
special.undefinedundefined
All other types are called "primitive" because their values can contain only a single thing (be it a string or a number or whatever). In contrast, objects are used to store collections of data and more complex entities.
undefinedundefinedBeing that important, objects deserve a special treatment. We'll deal with them later in the chapter undefinedundefinedinfo:object, after we learn more about primitives.undefinedundefined
undefinedundefinedThe
undefinedundefinedsymbol type is used to create unique identifiers for objects. We have to mention it
here for the sake of completeness, but also postpone the details till we know objects.undefinedundefined
The undefinedundefinedtypeof operator returns the type of the argument. It's useful when we want to
process values of different types differently or just want to do a quick check.undefinedundefined
It supports two forms of syntax:
undefinedundefinedtypeof x.undefinedundefinedtypeof(x).undefinedundefinedIn other words, it works with parentheses or without them. The result is the same.
undefinedundefinedThe call to
undefinedundefinedtypeof x returns a string with the type name:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedtypeofundefinedundefinedundefinedundefinedundefined// "undefined"undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedtypeofundefinedundefined0undefinedundefined// "number"undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedtypeof 10n undefinedundefined// "bigint"undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedtypeofundefinedundefinedtrueundefinedundefined// "boolean"undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedtypeofundefinedundefined"foo"undefinedundefined// "string"undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedtypeofundefinedundefinedSymbol(undefinedundefined"id") undefinedundefined// "symbol"undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedtypeof Math undefinedundefined// "object" (1)undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefinedtypeof null // "object" undefinedundefined(undefinedundefined2undefinedundefined)undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/undefinedundefined!*undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefinedundefinedundefinedtypeof alert undefinedundefined// "function" (3)undefinedundefinedundefinedundefinedundefinedundefined*undefinedundefined/!undefinedundefined*undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The last three lines may need additional explanation:
undefinedundefinedMath is a built-in object that provides mathematical operations.
We will learn it in the chapter undefinedundefinedinfo:number. Here, it serves
just as an example of an object.undefinedundefinedtypeof null is undefinedundefined"object". That's an officially
recognized error in undefinedundefinedtypeof behavior, coming from the early days of JavaScript and
kept for compatibility. Definitely, undefinedundefinednull is not an object. It is a special value with
a separate type of its own.undefinedundefinedtypeof alert is undefinedundefined"function", because
undefinedundefinedalert is a function. We'll study functions in the next chapters where we'll also see
that there's no special "function" type in JavaScript. Functions belong to the object type. But
undefinedundefinedtypeof treats them differently, returning undefinedundefined"function".
That also comes from the early days of JavaScript. Technically, such behavior isn't correct, but can be convenient
in practice.undefinedundefinedThere are 8 basic data types in JavaScript.
undefinedundefinednumber for numbers of any kind: integer or floating-point, integers are limited by
undefinedundefined±(2undefinedundefined53-1)undefinedundefined.undefinedundefinedbigint is for integer numbers of arbitrary
length.undefinedundefinedstring for strings. A string may
have zero or more characters, there's no separate single-character type.undefinedundefinedboolean for
undefinedundefinedtrue/undefinedundefinedfalse.undefinedundefinednull for unknown values - a standalone type that has a single value
undefinedundefinednull.undefinedundefinedundefined for unassigned values - a standalone type that has a single value
undefinedundefinedundefined.undefinedundefinedobject for more complex data structures.undefinedundefinedsymbol for unique identifiers.undefinedundefinedThe undefinedundefinedtypeof operator allows us to see which type is stored in a
variable.undefinedundefined
typeof x or undefinedundefinedtypeof(x).undefinedundefined"string".undefinedundefinednull returns undefinedundefined"object" - this is an error in the
language, it's not actually an object.undefinedundefinedIn the next chapters, we'll concentrate on primitive values and once we're familiar with them, we'll move on to objects.
undefinedundefinedlibs: - lodash
undefinedundefinedundefinedundefinedCurrying is an advanced technique of working with functions. It's used not only in JavaScript, but in other languages as well.undefinedundefined
undefinedundefinedCurrying is a
transformation of functions that translates a function from callable as undefinedundefinedf(a, b, c) into
callable as undefinedundefinedf(a)(b)(c).undefinedundefined
Currying doesn't call a function. It just transforms it.
undefinedundefinedLet's see an example first, to better understand what we're talking about, and then practical applications.
undefinedundefinedWe'll create a helper function
undefinedundefinedcurry(f) that performs currying for a two-argument undefinedundefinedf. In
other words, undefinedundefinedcurry(f) for two-argument undefinedundefinedf(a, b)
translates it into a function that runs as undefinedundefinedf(a)(b):undefinedundefined
run undefinedundefined! function curry(f) { // curry(f) does the currying transform return function(a) { return function(b) { return f(a, b); }; }; } undefinedundefined/!undefinedundefined
undefinedundefined// usage function sum(a, b) { return a + b; }
undefinedundefinedlet curriedSum = curry(sum);
undefinedundefinedalert( curriedSum(1)(2) ); // 3
undefinedundefinedAs you can see, the implementation is straightforward: it's just two wrappers.
undefinedundefinedcurry(func) is a wrapper
undefinedundefinedfunction(a).undefinedundefinedcurriedSum(1), the argument is saved in the Lexical Environment, and a new wrapper is
returned undefinedundefinedfunction(b).undefinedundefined2 as an argument, and it passes the call to the original
undefinedundefinedsum.undefinedundefinedMore advanced implementations of currying, such as undefinedundefined_.curry from lodash library, return a wrapper that allows a function to be called both normally and partially:undefinedundefined
undefinedundefinedrun function sum(a, b) { return a + b; }
undefinedundefinedlet curriedSum = undefinedundefined.curry(sum); // using .curry from lodash libraryundefinedundefined
undefinedundefinedalert( curriedSum(1, 2) ); // 3, still callable normally alert( curriedSum(1)(2) ); // 3, called partially
undefinedundefinedTo understand the benefits we need a worthy real-life example.
undefinedundefinedFor instance, we have the logging function
undefinedundefinedlog(date, importance, message) that formats and outputs the information. In real
projects such functions have many useful features like sending logs over the network, here we'll just use
undefinedundefinedalert:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedlog(dateundefinedundefined, importanceundefinedundefined, message) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedalert(undefinedundefined`[undefinedundefined${undefinedundefineddate.undefinedundefinedgetHours()undefinedundefined}undefinedundefined:undefinedundefined${undefinedundefineddate.undefinedundefinedgetMinutes()undefinedundefined}undefinedundefined] [undefinedundefined${importanceundefinedundefined}undefinedundefined] undefinedundefined${messageundefinedundefined}undefinedundefined`)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Let's curry it!
undefinedundefinedundefinedundefinedundefinedundefinedlog undefinedundefined=undefinedundefined_.undefinedundefinedcurry(log)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
After that undefinedundefinedlog works normally:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedlog(undefinedundefinednewundefinedundefinedDate()undefinedundefined,undefinedundefined"DEBUG"undefinedundefined,undefinedundefined"some debug")undefinedundefined;undefinedundefined// log(a, b, c)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
…But also works in the curried form:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlog(undefinedundefinednewundefinedundefinedDate())(undefinedundefined"DEBUG")(undefinedundefined"some debug")undefinedundefined;undefinedundefined// log(a)(b)(c)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Now we can easily make a convenience function for current logs:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// logNow will be the partial of log with fixed first argumentundefinedundefinedundefinedundefinedundefinedundefinedlet logNow undefinedundefined=undefinedundefinedlog(undefinedundefinednewundefinedundefinedDate())undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined// use itundefinedundefinedundefinedundefinedundefinedundefinedlogNow(undefinedundefined"INFO"undefinedundefined,undefinedundefined"message")undefinedundefined;undefinedundefined// [HH:mm] INFO messageundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Now undefinedundefinedlogNow is undefinedundefinedlog with fixed
first argument, in other words "partially applied function" or "partial" for short.undefinedundefined
We can go further and make a convenience function for current debug logs:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet debugNow undefinedundefined=undefinedundefinedlogNow(undefinedundefined"DEBUG")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefineddebugNow(undefinedundefined"message")undefinedundefined;undefinedundefined// [HH:mm] DEBUG messageundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
So: 1. We didn't lose anything after currying: undefinedundefinedlog is still
callable normally. 2. We can easily generate partial functions such as for today's logs.undefinedundefined
In case you'd like to get in to the details, here's the "advanced" curry implementation for multi-argument functions that we could use above.
undefinedundefinedIt's pretty short:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedcurry(func) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedfunctionundefinedundefinedcurried(...undefinedundefinedargs) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (undefinedundefinedargs.undefinedundefinedlengthundefinedundefined>=undefinedundefinedfunc.undefinedundefinedlength) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedfunc.undefinedundefinedapply(undefinedundefinedthisundefinedundefined, args)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedelseundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedfunction(...undefinedundefinedargs2) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedcurried.undefinedundefinedapply(undefinedundefinedthisundefinedundefined,undefinedundefinedargs.undefinedundefinedconcat(args2))undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Usage examples:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedsum(aundefinedundefined, bundefinedundefined, c) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturn a undefinedundefined+ b undefinedundefined+ cundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedlet curriedSum undefinedundefined=undefinedundefinedcurry(sum)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefinedcurriedSum(undefinedundefined1undefinedundefined,undefinedundefined2undefinedundefined,undefinedundefined3) )undefinedundefined;undefinedundefined// 6, still callable normallyundefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefinedcurriedSum(undefinedundefined1)(undefinedundefined2undefinedundefined,undefinedundefined3) )undefinedundefined;undefinedundefined// 6, currying of 1st argundefinedundefinedundefinedundefinedundefinedundefinedalert( undefinedundefinedcurriedSum(undefinedundefined1)(undefinedundefined2)(undefinedundefined3) )undefinedundefined;undefinedundefined// 6, full curryingundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
The new undefinedundefinedcurry may look complicated, but it's actually easy to
understand.undefinedundefined
The result of undefinedundefinedcurry(func) call is
the wrapper undefinedundefinedcurried that looks like this:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// func is the function to transformundefinedundefinedundefinedundefinedundefinedundefinedfunctionundefinedundefinedcurried(...undefinedundefinedargs) undefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedif (undefinedundefinedargs.undefinedundefinedlengthundefinedundefined>=undefinedundefinedfunc.undefinedundefinedlength) undefinedundefined{undefinedundefined// (1)undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedfunc.undefinedundefinedapply(undefinedundefinedthisundefinedundefined, args)undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedelseundefinedundefined{undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedfunction(...undefinedundefinedargs2) undefinedundefined{undefinedundefined// (2)undefinedundefinedundefinedundefinedundefinedundefinedreturnundefinedundefinedcurried.undefinedundefinedapply(undefinedundefinedthisundefinedundefined,undefinedundefinedargs.undefinedundefinedconcat(args2))undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined}undefinedundefinedundefinedundefinedundefinedundefined};undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
When we run it, there are two undefinedundefinedif execution
branches:undefinedundefined
args count is the same or more than the original function has in its definition
(undefinedundefinedfunc.length) , then just pass the call to it using
undefinedundefinedfunc.apply.undefinedundefinedfunc just yet. Instead, another wrapper is returned, that will re-apply
undefinedundefinedcurried providing previous arguments together with the new ones.undefinedundefined
Then, if we call it, again, we'll get either a new partial (if not enough arguments) or, finally, the result.
undefinedundefinedsmart header="Fixed-length functions only" The currying requires the function to have a fixed number of arguments.
undefinedundefinedA function that uses rest parameters, such as undefinedundefinedf(...args), can't be
curried this way.
undefinedundefined
``undefinedundefinedsmart header="A little more than currying" By definition, currying should convertsum(a,
b, c)undefinedundefinedintosum(a)(b)(c)`.undefinedundefined
But most implementations of currying in JavaScript are advanced, as described: they also keep the function callable in the multi-argument variant.
undefinedundefinedundefinedundefinedCurrying is a
transform that makes undefinedundefinedf(a,b,c) callable as undefinedundefinedf(a)(b)(c).
JavaScript implementations usually both keep the function callable normally and return the partial if the arguments
count is not enough.undefinedundefined
Currying allows us to easily get partials. As we've
seen in the logging example, after currying the three argument universal function
undefinedundefinedlog(date, importance, message) gives us partials when called with one argument (like
undefinedundefinedlog(date)) or two arguments (like
undefinedundefinedlog(date, importance)).undefinedundefined
warn header="In-depth language feature" This article covers an advanced topic, to understand certain edge-cases better.
undefinedundefinedIt's not important. Many experienced developers live fine without knowing it. Read on if you're want to know how things work under the hood.
undefinedundefinedA dynamically evaluated method call can lose
undefinedundefinedthis.undefinedundefined
For instance:
undefinedundefinedrun let user = { name: "John", hi() { alert(this.name); }, bye() { alert("Bye"); } };
undefinedundefineduser.hi(); // works
undefinedundefined// now let's call user.hi or user.bye depending on the name undefinedundefined! (user.name == "John" ? user.hi : user.bye)(); // Error! undefinedundefined/! undefinedundefined
undefinedundefinedOn the last line there is a conditional operator that chooses either
undefinedundefineduser.hi or undefinedundefineduser.bye. In this case the result is
undefinedundefineduser.hi.undefinedundefined
Then the method is immediately
called with parentheses undefinedundefined(). But it doesn't work correctly!undefinedundefined
As you can see, the call results in an error, because the value of
undefinedundefined"this" inside the call becomes
undefinedundefinedundefined.undefinedundefined
This works (object dot method):
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefineduser.undefinedundefinedhi()undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
This doesn't (evaluated method):
undefinedundefinedundefinedundefinedundefinedundefined(undefinedundefineduser.undefinedundefinednameundefinedundefined==undefinedundefined"John"undefinedundefined?undefinedundefineduser.undefinedundefinedhi : undefinedundefineduser.undefinedundefinedbye)()undefinedundefined;undefinedundefined// Error!undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
Why? If we want to understand why it happens, let's get under the hood of how
undefinedundefinedobj.method() call works.undefinedundefined
Looking closely, we may notice two
operations in undefinedundefinedobj.method() statement:undefinedundefined
'.' retrieves the property
undefinedundefinedobj.method.undefinedundefined() execute it.undefinedundefinedSo,
how does the information about undefinedundefinedthis get passed from the first part to the second
one?undefinedundefined
If we put these operations on separate lines, then
undefinedundefinedthis will be lost for sure:undefinedundefined
run let user = { name: "John", hi() { alert(this.name); } }
undefinedundefinedundefinedundefined! // split getting and calling the method in two lines let hi = user.hi; hi(); // Error, because this is undefined undefinedundefined/! undefinedundefined
undefinedundefinedHere undefinedundefinedhi = user.hi puts the function into the
variable, and then on the last line it is completely standalone, and so there's no
undefinedundefinedthis.undefinedundefined
undefinedundefinedTo make
undefinedundefineduser.hi() calls work, JavaScript uses a trick - the dot
undefinedundefined'.' returns not a function, but a value of the special undefinedundefinedReference
Type.undefinedundefinedundefinedundefined
The Reference Type is a "specification type". We can't explicitly use it, but it is used internally by the language.
undefinedundefined
The value of Reference Type is a three-value combination undefinedundefined(base, name, strict),
where:undefinedundefined
base is the
object.undefinedundefinedname is the property
name.undefinedundefinedstrict is true if
undefinedundefineduse strict is in effect.undefinedundefinedThe result of a property access undefinedundefineduser.hi is not a function, but a
value of Reference Type. For undefinedundefineduser.hi in strict mode it is:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined// Reference Type valueundefinedundefinedundefinedundefined(userundefinedundefined,undefinedundefined"hi"undefinedundefined,undefinedundefinedtrue)undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
When parentheses undefinedundefined() are called on the Reference Type, they
receive the full information about the object and its method, and can set the right
undefinedundefinedthis (undefinedundefined=user in this case).undefinedundefined
Reference type is a special "intermediary" internal type, with the purpose to pass information from
dot undefinedundefined. to calling parentheses undefinedundefined().undefinedundefined
Any other operation like assignment undefinedundefinedhi = user.hi discards the
reference type as a whole, takes the value of undefinedundefineduser.hi (a function) and passes it on. So
any further operation "loses" undefinedundefinedthis.undefinedundefined
So, as
the result, the value of undefinedundefinedthis is only passed the right way if the function is called
directly using a dot undefinedundefinedobj.method() or square brackets
undefinedundefinedobj['method']() syntax (they do the same here). There are various ways to solve this
problem such as undefinedundefinedfunc.bind().undefinedundefined
Reference Type is an internal type of the language.
undefinedundefinedReading a property, such as with dot undefinedundefined. in
undefinedundefinedobj.method() returns not exactly the property value, but a special "reference type"
value that stores both the property value and the object it was taken from.undefinedundefined
That's for the subsequent method call undefinedundefined() to get the object and set
undefinedundefinedthis to it.undefinedundefined
For all other operations, the reference type automatically becomes the property value (a function in our case).
undefinedundefinedThe whole mechanics is hidden from our eyes. It only matters in subtle cases, such as when a method is obtained dynamically from the object, using an expression.
undefinedundefined[recent caniuse="bigint"]
undefinedundefinedundefinedundefinedBigInt is a special numeric type that
provides support for integers of arbitrary length.undefinedundefined
A bigint is created by
appending undefinedundefinedn to the end of an integer literal or by calling the function
undefinedundefinedBigInt that creates bigints from strings, numbers etc.undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefinedconst bigint undefinedundefined= 1234567890123456789012345678901234567890nundefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedconst sameBigint undefinedundefined=undefinedundefinedBigInt(undefinedundefined"1234567890123456789012345678901234567890")undefinedundefined;undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedconst bigintFromNumber undefinedundefined=undefinedundefinedBigInt(undefinedundefined10)undefinedundefined;undefinedundefined// same as 10nundefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
undefinedundefinedBigInt can mostly be used like a regular number, for example:undefinedundefined
run alert(1n + 2n); // 3
undefinedundefinedalert(5n / 2n); // 2
undefinedundefinedPlease note: the division undefinedundefined5/2 returns the result rounded towards
zero, without the decimal part. All operations on bigints return bigints.undefinedundefined
We can't mix bigints and regular numbers:
undefinedundefined
undefinedundefinedjs run alert(1n + 2); // Error: Cannot mix BigInt and other typesundefinedundefined
We should explicitly convert them if needed: using either undefinedundefinedBigInt()
or undefinedundefinedNumber(), like this:undefinedundefined
run let bigint = 1n; let number = 2;
undefinedundefined// number to bigint alert(bigint + BigInt(number)); // 3
undefinedundefined// bigint to number alert(Number(bigint) + number); // 3
undefinedundefinedThe conversion operations are always silent, never give errors, but if the bigint is too huge and won't fit the number type, then extra bits will be cut off, so we should be careful doing such conversion.
undefinedundefined
undefinedundefinedsmart header="The unary plus is not supported on bigints" The unary plus operator+valueundefinedundefinedis a well-known way to convertvalue`
to a number.undefinedundefined
In order to avoid confusion, it's not supported on bigints: run let bigint = 1n;
undefinedundefinedalert( +bigint ); // error
undefinedundefinedundefinedundefinedSo we should use `Number()` to convert a bigint to a number.undefinedundefined
undefinedundefinedComparisons, such as
undefinedundefined<, undefinedundefined> work with bigints and numbers just
fine:undefinedundefined
run alert( 2n > 1n ); // true
undefinedundefinedalert( 2n > 1 ); // true
undefinedundefinedPlease note though, as numbers and bigints belong to different types, they can be equal
undefinedundefined==, but not strictly equal undefinedundefined===:undefinedundefined
run alert( 1 == 1n ); // true
undefinedundefinedalert( 1 === 1n ); // false
undefinedundefinedWhen inside
undefinedundefinedif or other boolean operations, bigints behave like numbers.undefinedundefined
For instance, in undefinedundefinedif, bigint undefinedundefined0n is
falsy, other values are truthy:undefinedundefined
undefinedundefinedjs run if (0n) { // never executes }undefinedundefined
Boolean operators, such as undefinedundefined||, undefinedundefined&& and others
also work with bigints similar to numbers:undefinedundefined
run alert( 1n || 2 ); // 1 (1n is considered truthy)
undefinedundefinedalert( 0n || 2 ); // 2 (0n is considered falsy)
undefinedundefinedPolyfilling bigints is tricky. The reason is
that many JavaScript operators, such as undefinedundefined+, undefinedundefined- and so on
behave differently with bigints compared to regular numbers.undefinedundefined
For example, division of bigints always returns a bigint (rounded if necessary).
undefinedundefinedTo emulate such behavior, a polyfill would need to analyze the code and replace all such operators with its functions. But doing so is cumbersome and would cost a lot of performance.
undefinedundefinedSo, there's no well-known good polyfill.
undefinedundefinedAlthough, the other way around is proposed by the developers of undefinedundefinedJSBI library.undefinedundefined
undefinedundefinedThis library implements big numbers using its own methods. We can use them instead of native bigints:
undefinedundefined| Operation | undefinedundefinednative undefinedundefinedBigIntundefinedundefined | undefinedundefinedJSBI | undefinedundefined
|---|---|---|
| Creation from Number | undefinedundefined
undefinedundefineda = BigInt(789)undefinedundefined | undefinedundefined
undefinedundefineda = JSBI.BigInt(789)undefinedundefined | undefinedundefined
| Addition | undefinedundefined
undefinedundefinedc = a + bundefinedundefined | undefinedundefined
undefinedundefinedc = JSBI.add(a, b)undefinedundefined | undefinedundefined
| Subtraction | undefinedundefined
undefinedundefinedc = a - bundefinedundefined | undefinedundefined
undefinedundefinedc = JSBI.subtract(a, b)undefinedundefined | undefinedundefined
| … | undefinedundefined… | undefinedundefined… | undefinedundefined
…And then use the polyfill (Babel plugin) to convert JSBI calls to native bigints for those browsers that support them.
undefinedundefinedIn other words, this approach suggests that we write code in JSBI instead of native bigints. But JSBI works with numbers as with bigints internally, emulates them closely following the specification, so the code will be "bigint-ready".
undefinedundefinedWe can use such JSBI code "as is" for engines that don't support bigints and for those that do support - the polyfill will convert the calls to native bigints.
undefinedundefinedThe JavaScript language was initially created for web browsers. Since then it has evolved and become a language with many uses and platforms.
undefinedundefinedA platform may be a browser, or a web-server or another undefinedundefinedhost, even a "smart" coffee machine, if it can run JavaScript. Each of them provides platform-specific functionality. The JavaScript specification calls that a undefinedundefinedhost environment.undefinedundefined
undefinedundefinedA host environment provides own objects and functions additional to the language core. Web browsers give a means to control web pages. Node.js provides server-side features, and so on.
undefinedundefinedHere's a bird's-eye view of what we have when JavaScript runs in a web browser:
undefinedundefinedundefinedundefinedundefinedundefined
There's a "root" object called undefinedundefinedwindow. It has two
roles:undefinedundefined
For instance, here we use it as a global object:
undefinedundefinedrun function sayHi() { alert("Hello"); }
undefinedundefined// global functions are methods of the global object: window.sayHi();
undefinedundefinedAnd here we use it as a browser window, to see the window height:
undefinedundefined
undefinedundefinedjs run alert(window.innerHeight); // inner window heightundefinedundefined
There are more window-specific methods and properties, we'll cover them later.
undefinedundefinedDocument Object Model, or DOM for short, represents all page content as objects that can be modified.
undefinedundefined
The undefinedundefineddocument object is the main "entry point" to the page. We can change or create
anything on the page using it.undefinedundefined
For instance: run // change the background color to red document.body.style.background = "red";
undefinedundefined// change it back after 1 second setTimeout(() => document.body.style.background = "", 1000);
undefinedundefinedHere we used undefinedundefineddocument.body.style, but there's much, much more.
Properties and methods are described in the specification: undefinedundefinedDOM
Living Standard.undefinedundefined
smart header="DOM is not only for browsers" The DOM specification explains the structure of a document and provides objects to manipulate it. There are non-browser instruments that use DOM too.
undefinedundefinedFor instance, server-side scripts that download HTML pages and process them can also use DOM. They may support only a part of the specification though.
undefinedundefinedsmart header="CSSOM for styling" There's also a separate specification, undefinedundefinedCSS Object Model (CSSOM) for CSS rules and stylesheets, that explains how they are represented as objects, and how to read and write them.undefinedundefined
undefinedundefinedCSSOM is used together with DOM when we modify style rules for the document. In practice though, CSSOM is rarely required, because we rarely need to modify CSS rules from JavaScript (usually we just add/remove CSS classes, not modify their CSS rules), but that's also possible.
undefinedundefinedThe Browser Object Model (BOM) represents additional objects provided by the browser (host environment) for working with everything except the document.
undefinedundefinedFor instance:
undefinedundefinednavigator.userAgent - about the current browser, and
undefinedundefinednavigator.platform - about the platform (can help to differ between Windows/Linux/Mac
etc).undefinedundefinedHere's how we can use the
undefinedundefinedlocation object:undefinedundefined
undefinedundefinedjs run alert(location.href); // shows current URL if (confirm("Go to Wikipedia?")) { location.href = "https://wikipedia.org"; // redirect the browser to another URL }undefinedundefined
Functions undefinedundefinedalert/confirm/prompt are also a part of BOM: they are
directly not related to the document, but represent pure browser methods of communicating with the
user.undefinedundefined
smart header="Specifications" BOM is the part of the general undefinedundefinedHTML specification.undefinedundefined
undefinedundefinedYes, you heard that right. The HTML spec at undefinedundefinedhttps://html.spec.whatwg.org is not only about the "HTML language" (tags, attributes), but also covers a bunch of objects, methods and browser-specific DOM extensions. That's "HTML in broad terms". Also, some parts have additional specs listed at undefinedundefinedhttps://spec.whatwg.org. undefinedundefined
undefinedundefinedTalking about standards, we have:
undefinedundefinedsetTimeout, undefinedundefinedalert,
undefinedundefinedlocation and so on, see undefinedundefinedhttps://html.spec.whatwg.org. It takes the DOM specification and extends it with many additional
properties and methods.
undefinedundefinedAdditionally, some classes are described separately at undefinedundefinedhttps://spec.whatwg.org/.undefinedundefined
undefinedundefinedPlease note these links, as there's so much stuff to learn it's impossible to cover and remember everything.
undefinedundefinedWhen you'd like to read about a property or a method, the Mozilla manual at undefinedundefinedhttps://developer.mozilla.org/en-US/search is also a nice resource, but the corresponding spec may be better: it's more complex and longer to read, but will make your fundamental knowledge sound and complete.undefinedundefined
undefinedundefinedTo find something, it's often convenient to use an internet search "WHATWG [term]" or "MDN [term]", e.g undefinedundefinedhttps://google.com?q=whatwg+localstorage, undefinedundefinedhttps://google.com?q=mdn+localstorage.undefinedundefined
undefinedundefinedNow we'll get down to learning DOM, because the document plays the central role in the UI.
undefinedundefinedlibs: - d3 - domtree
undefinedundefinedThe backbone of an HTML document is tags.
undefinedundefinedAccording to the Document Object Model (DOM), every HTML tag is an object. Nested tags are "children" of the enclosing one. The text inside a tag is an object as well.
undefinedundefinedAll these objects are accessible using JavaScript, and we can use them to modify the page.
undefinedundefinedFor example,
undefinedundefineddocument.body is the object representing the
undefinedundefined<body> tag.undefinedundefined
Running this code will make
the undefinedundefined<body> red for 3 seconds:undefinedundefined
run document.body.style.background = ‘red'; // make the background red
undefinedundefinedsetTimeout(() => document.body.style.background = '', 3000); // return back
undefinedundefinedHere we used undefinedundefinedstyle.background to change the background color of
undefinedundefineddocument.body, but there are many other properties, such as:undefinedundefined
innerHTML - HTML contents of the
node.undefinedundefinedoffsetWidth - the node width (in
pixels)undefinedundefinedSoon we'll learn more ways to manipulate the DOM, but first we need to know about its structure.
undefinedundefinedLet's start with the following simple document:
undefinedundefined
undefinedundefinedhtml run no-beautify <!DOCTYPE HTML> <html> <head> <title>About elk</title> </head> <body> The truth about elk. </body> </html>undefinedundefined
The DOM represents HTML as a tree structure of tags. Here's how it looks:
undefinedundefined undefinedundefined undefinedundefinedundefinedundefinedOn the picture above, you can click on element nodes and their children will open/collapse.undefinedundefined
undefinedundefinedEvery tree node is an object.
undefinedundefinedTags are undefinedundefinedelement
nodes (or just elements) and form the tree structure: undefinedundefined<html> is at the
root, then undefinedundefined<head> and undefinedundefined<body> are its
children, etc.undefinedundefined
The text inside elements forms undefinedundefinedtext
nodes, labelled as undefinedundefined#text. A text node contains only a string. It may not have
children and is always a leaf of the tree.undefinedundefined
For instance, the
undefinedundefined<title> tag has the text
undefinedundefined"About elk".undefinedundefined
Please note the special characters in text nodes:
undefinedundefined↵
(in JavaScript known as undefinedundefined\n)undefinedundefined␣undefinedundefinedSpaces and
newlines are totally valid characters, like letters and digits. They form text nodes and become a part of the DOM. So,
for instance, in the example above the undefinedundefined<head> tag contains some spaces before
undefinedundefined<title>, and that text becomes a undefinedundefined#text node (it
contains a newline and some spaces only).undefinedundefined
There are only two top-level
exclusions: 1. Spaces and newlines before undefinedundefined<head> are ignored for historical
reasons. 2. If we put something after undefinedundefined</body>, then that is automatically moved
inside the undefinedundefinedbody, at the end, as the HTML spec requires that all content must be inside
undefinedundefined<body>. So there can't be any spaces after
undefinedundefined</body>.undefinedundefined
In other cases everything's straightforward - if there are spaces (just like any character) in the document, then they become text nodes in the DOM, and if we remove them, then there won't be any.
undefinedundefinedHere are no space-only text nodes:
undefinedundefined
undefinedundefinedhtml no-beautify <!DOCTYPE HTML> <html><head><title>About elk</title></head><body>The truth about elk.</body></html>undefinedundefined
smart header="Spaces at string start/end and space-only text nodes are usually hidden in tools" Browser tools (to be covered soon) that work with DOM usually do not show spaces at the start/end of the text and empty text nodes (line-breaks) between tags.
undefinedundefinedDeveloper tools save screen space this way.
undefinedundefinedOn further DOM pictures we'll sometimes omit them when they are irrelevant. Such spaces usually do not affect how the document is displayed.
undefinedundefinedIf the browser encounters malformed HTML, it automatically corrects it when making the DOM.
undefinedundefinedFor instance, the top tag is
always undefinedundefined<html>. Even if it doesn't exist in the document, it will exist in the
DOM, because the browser will create it. The same goes for
undefinedundefined<body>.undefinedundefined
As an example, if the HTML file
is the single word undefinedundefined"Hello", the browser will wrap it into
undefinedundefined<html> and undefinedundefined<body>, and add the required
undefinedundefined<head>, and the DOM will be:undefinedundefined
While generating the DOM, browsers automatically process errors in the document, close tags and so on.
undefinedundefinedA document with unclosed tags:
undefinedundefined
undefinedundefinedhtml no-beautify <p>Hello <li>Mom <li>and <li>Dadundefinedundefined
…will become a normal DOM as the browser reads tags and restores the missing parts:
undefinedundefinedundefinedundefined undefinedundefinedwarn header="Tables always haveundefinedundefined
undefinedundefined" An interesting "special case" is tables. By DOM specification they must haveundefinedundefined
undefinedundefinedtag, but HTML text may omit it. Then the browser createsundefinedundefined
undefinedundefined` in the DOM automatically.
undefinedundefinedFor the HTML:
undefinedundefined
undefinedundefinedhtml no-beautify <table id="table"><tr><td>1</td></tr></table>undefinedundefined
You see? The undefinedundefined<tbody> appeared out of nowhere. We
should keep this in mind while working with tables to avoid surprises.
undefinedundefined
There are some other node types besides elements and text nodes.
undefinedundefinedFor example, comments:
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined<!DOCTYPE HTMLundefinedundefined>undefinedundefinedundefinedundefinedundefinedundefined<html>undefinedundefinedundefinedundefinedundefinedundefined<body>undefinedundefinedundefinedundefined The truth about elk.undefinedundefinedundefinedundefined<ol>undefinedundefinedundefinedundefinedundefinedundefined<li>An elk is a smartundefinedundefined</li>undefinedundefinedundefinedundefined*!*undefinedundefinedundefinedundefined<!-- comment -->undefinedundefinedundefinedundefined*/!*undefinedundefinedundefinedundefined<li>...and cunning animal!undefinedundefined</li>undefinedundefinedundefinedundefinedundefinedundefined</ol>undefinedundefinedundefinedundefinedundefinedundefined</body>undefinedundefinedundefinedundefinedundefinedundefined</html>undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefined
We can see here a new tree node type - undefinedundefinedcomment node, labeled
as undefinedundefined#comment, between two text nodes.undefinedundefined
We may think - why is a comment added to the DOM? It doesn't affect the visual representation in any way. But there's a rule - if something's in HTML, then it also must be in the DOM tree.
undefinedundefinedundefinedundefinedEverything in HTML, even comments, becomes a part of the DOM.undefinedundefined
undefinedundefinedEven the undefinedundefined<!DOCTYPE...> directive at the very beginning
of HTML is also a DOM node. It's in the DOM tree right before undefinedundefined<html>. Few
people know about that. We are not going to touch that node, we even don't draw it on diagrams, but it's
there.undefinedundefined
The undefinedundefineddocument object that represents
the whole document is, formally, a DOM node as well.undefinedundefined
There are undefinedundefined12 node types. In practice we usually work with 4 of them:undefinedundefined
undefinedundefineddocument - the "entry point" into DOM.undefinedundefinedTo see the DOM structure in real-time, try undefinedundefinedLive DOM Viewer. Just type in the document, and it will show up as a DOM at an instant.undefinedundefined
undefinedundefinedAnother way to explore the DOM is to use the browser developer tools. Actually, that's what we use when developing.
undefinedundefinedTo do so, open the web page undefinedundefinedelk.html, turn on the browser developer tools and switch to the Elements tab.undefinedundefined
undefinedundefinedIt should look like this:
undefinedundefined
undefinedundefinedundefinedundefined
You can see the DOM, click on elements, see their details and so on.
undefinedundefinedPlease note that the DOM structure in developer tools is simplified. Text nodes are shown just as text. And there are no "blank" (space only) text nodes at all. That's fine, because most of the time we are interested in element nodes.
undefinedundefinedClicking the undefinedundefined button in the left-upper corner allows us to choose a node from the webpage using a mouse (or other pointer devices) and "inspect" it (scroll to it in the Elements tab). This works great when we have a huge HTML page (and corresponding huge DOM) and would like to see the place of a particular element in it.undefinedundefined
undefinedundefinedAnother way to do it would be just right-clicking on a webpage and selecting "Inspect" in the context menu.
undefinedundefined
undefinedundefinedundefinedundefined
At the right part of the tools there are the following subtabs: - undefinedundefinedStyles - we can see CSS applied to the current element rule by rule, including built-in rules (gray). Almost everything can be edited in-place, including the dimensions/margins/paddings of the box below. - undefinedundefinedComputed - to see CSS applied to the element by property: for each property we can see a rule that gives it (including CSS inheritance and such). - undefinedundefinedEvent Listeners - to see event listeners attached to DOM elements (we'll cover them in the next part of the tutorial). - …and so on.undefinedundefined
undefinedundefinedThe best way to study them is to click around. Most values are editable in-place.
undefinedundefinedAs we work the DOM, we also may want to apply JavaScript to it. Like: get a node and run some code to modify it, to see the result. Here are few tips to travel between the Elements tab and the console.
undefinedundefinedFor the start:
undefinedundefined<li> in the Elements
tab.undefinedundefinedkey:Esc - it will open
console right below the Elements tab.undefinedundefinedNow the
last selected element is available as undefinedundefined$0, the previously selected is
undefinedundefined$1 etc.undefinedundefined
We can run commands on them. For
instance, undefinedundefined$0.style.background = 'red' makes the selected list item red, like
this:undefinedundefined
undefinedundefinedundefinedundefined
That's how to get a node from Elements in Console.
undefinedundefinedThere's also a
road back. If there's a variable referencing a DOM node, then we can use the command
undefinedundefinedinspect(node) in Console to see it in the Elements pane.undefinedundefined
Or we can just output the DOM node in the console and explore "in-place", like
undefinedundefineddocument.body below:undefinedundefined
undefinedundefinedundefinedundefined
That's for debugging purposes of course. From the next chapter on we'll access and modify DOM using JavaScript.
undefinedundefinedThe browser developer tools are a great help in development: we can explore the DOM, try things and see what goes wrong.
undefinedundefinedAn HTML/XML document is represented inside the browser as the DOM tree.
undefinedundefinedWe can use developer tools to inspect DOM and modify it manually.
undefinedundefinedHere we covered the basics, the most used and important actions to start with. There's an extensive documentation about Chrome Developer Tools at undefinedundefinedhttps://developers.google.com/web/tools/chrome-devtools. The best way to learn the tools is to click here and there, read menus: most options are obvious. Later, when you know them in general, read the docs and pick up the rest.undefinedundefined
undefinedundefinedDOM nodes have properties and methods that allow us to travel between them, modify them, move around the page, and more. We'll get down to them in the next chapters.
undefinedundefinedlibs: - d3 - domtree
undefinedundefinedThe DOM allows us to do anything with elements and their contents, but first we need to reach the corresponding DOM object.
undefinedundefinedAll operations on the DOM start with the undefinedundefineddocument object. That's
the main "entry point" to DOM. From it we can access any node.undefinedundefined
Here's a picture of links that allow for travel between DOM nodes:
undefinedundefinedundefinedundefinedundefinedundefined
Let's discuss them in more detail.
undefinedundefined
The topmost tree nodes are available directly as undefinedundefineddocument
properties:undefinedundefined
<html> =
undefinedundefineddocument.documentElementundefinedundefineddocument.documentElement. That's the DOM node of the
undefinedundefined<html> tag.
undefinedundefined<body> =
undefinedundefineddocument.bodyundefinedundefined<body> element - undefinedundefineddocument.body.
undefinedundefined<head> =
undefinedundefineddocument.headundefinedundefined<head> tag is available as undefinedundefineddocument.head.
undefinedundefined
undefinedundefinedwarn header="There's a catch:document.bodyundefinedundefinedcan benull`"
A script cannot access an element that doesn't exist at the moment of running.undefinedundefined
In particular, if a script is inside undefinedundefined<head>, then
undefinedundefineddocument.body is unavailable, because the browser did not read it
yet.undefinedundefined
So, in the example below the first
undefinedundefinedalert shows undefinedundefinednull:undefinedundefined
undefinedundefinedundefinedundefinedundefinedundefined
undefinedundefinedsmart header="In the DOM world `null` means \"doesn't exist\"" In the DOM, the `null` value means "doesn't exist" or "no such node".undefinedundefined
There are two terms that we'll use from now on:
undefinedundefined<head> and
undefinedundefined<body> are children of undefinedundefined<html>
element.undefinedundefinedFor instance, here undefinedundefined<body> has
children undefinedundefined<div> and undefinedundefined<ul> (and few blank
text nodes):undefinedundefined